Skip to content

Commit 687978b

Browse files
authored
Reject all requests that have an unconsumed body (#37504)
This commit removes some leniency from REST handling where we move to reject all requests that have a body where the body is not used during the course of handling the request. For example, DELETE /index { "query" : { "term" : { "field" : "value" } } } is now rejected.
1 parent 75410dc commit 687978b

File tree

7 files changed

+165
-21
lines changed

7 files changed

+165
-21
lines changed

server/src/main/java/org/elasticsearch/rest/BaseRestHandler.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ public final void handleRequest(RestRequest request, RestChannel channel, NodeCl
9999
throw new IllegalArgumentException(unrecognized(request, unconsumedParams, candidateParams, "parameter"));
100100
}
101101

102+
if (request.hasContent() && request.isContentConsumed() == false) {
103+
throw new IllegalArgumentException("request [" + request.method() + " " + request.path() + "] does not support having a body");
104+
}
105+
102106
usageCount.increment();
103107
// execute the action
104108
action.accept(channel);

server/src/main/java/org/elasticsearch/rest/RestRequest.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ public class RestRequest implements ToXContent.Params {
6666
private final HttpRequest httpRequest;
6767
private final HttpChannel httpChannel;
6868

69+
private boolean contentConsumed = false;
70+
71+
public boolean isContentConsumed() {
72+
return contentConsumed;
73+
}
74+
6975
protected RestRequest(NamedXContentRegistry xContentRegistry, Map<String, String> params, String path,
7076
Map<String, List<String>> headers, HttpRequest httpRequest, HttpChannel httpChannel) {
7177
final XContentType xContentType;
@@ -173,10 +179,15 @@ public final String path() {
173179
}
174180

175181
public boolean hasContent() {
176-
return content().length() > 0;
182+
return content(false).length() > 0;
177183
}
178184

179185
public BytesReference content() {
186+
return content(true);
187+
}
188+
189+
protected BytesReference content(final boolean contentConsumed) {
190+
this.contentConsumed = this.contentConsumed | contentConsumed;
180191
return httpRequest.content();
181192
}
182193

server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestForceMergeAction.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
import static org.elasticsearch.rest.RestRequest.Method.POST;
3535

3636
public class RestForceMergeAction extends BaseRestHandler {
37-
public RestForceMergeAction(Settings settings, RestController controller) {
37+
38+
public RestForceMergeAction(final Settings settings, final RestController controller) {
3839
super(settings);
3940
controller.registerHandler(POST, "/_forcemerge", this);
4041
controller.registerHandler(POST, "/{index}/_forcemerge", this);
@@ -47,14 +48,12 @@ public String getName() {
4748

4849
@Override
4950
public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException {
50-
if (request.hasContent()) {
51-
throw new IllegalArgumentException("forcemerge takes arguments in query parameters, not in the request body");
52-
}
53-
ForceMergeRequest mergeRequest = new ForceMergeRequest(Strings.splitStringByCommaToArray(request.param("index")));
51+
final ForceMergeRequest mergeRequest = new ForceMergeRequest(Strings.splitStringByCommaToArray(request.param("index")));
5452
mergeRequest.indicesOptions(IndicesOptions.fromRequest(request, mergeRequest.indicesOptions()));
5553
mergeRequest.maxNumSegments(request.paramAsInt("max_num_segments", mergeRequest.maxNumSegments()));
5654
mergeRequest.onlyExpungeDeletes(request.paramAsBoolean("only_expunge_deletes", mergeRequest.onlyExpungeDeletes()));
5755
mergeRequest.flush(request.paramAsBoolean("flush", mergeRequest.flush()));
5856
return channel -> client.admin().indices().forceMerge(mergeRequest, new RestToXContentListener<>(channel));
5957
}
58+
6059
}

server/src/test/java/org/elasticsearch/action/admin/indices/forcemerge/RestForceMergeActionTests.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.elasticsearch.rest.RestController;
2929
import org.elasticsearch.rest.action.admin.indices.RestForceMergeAction;
3030
import org.elasticsearch.test.ESTestCase;
31+
import org.elasticsearch.test.rest.FakeRestChannel;
3132
import org.elasticsearch.test.rest.FakeRestRequest;
3233

3334
import static org.hamcrest.Matchers.equalTo;
@@ -39,9 +40,12 @@ public void testBodyRejection() throws Exception {
3940
final RestForceMergeAction handler = new RestForceMergeAction(Settings.EMPTY, mock(RestController.class));
4041
String json = JsonXContent.contentBuilder().startObject().field("max_num_segments", 1).endObject().toString();
4142
final FakeRestRequest request = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY)
42-
.withContent(new BytesArray(json), XContentType.JSON).build();
43+
.withContent(new BytesArray(json), XContentType.JSON)
44+
.withPath("/_forcemerge")
45+
.build();
4346
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
44-
() -> handler.prepareRequest(request, mock(NodeClient.class)));
45-
assertThat(e.getMessage(), equalTo("forcemerge takes arguments in query parameters, not in the request body"));
47+
() -> handler.handleRequest(request, new FakeRestChannel(request, randomBoolean(), 1), mock(NodeClient.class)));
48+
assertThat(e.getMessage(), equalTo("request [GET /_forcemerge] does not support having a body"));
4649
}
50+
4751
}

server/src/test/java/org/elasticsearch/rest/BaseRestHandlerTests.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@
2121

2222
import org.elasticsearch.client.node.NodeClient;
2323
import org.elasticsearch.common.Table;
24+
import org.elasticsearch.common.bytes.BytesArray;
2425
import org.elasticsearch.common.settings.Settings;
26+
import org.elasticsearch.common.xcontent.XContentBuilder;
27+
import org.elasticsearch.common.xcontent.XContentType;
28+
import org.elasticsearch.common.xcontent.json.JsonXContent;
2529
import org.elasticsearch.rest.action.cat.AbstractCatAction;
2630
import org.elasticsearch.test.ESTestCase;
2731
import org.elasticsearch.test.rest.FakeRestChannel;
@@ -232,4 +236,78 @@ public String getName() {
232236
assertTrue(executed.get());
233237
}
234238

239+
public void testConsumedBody() throws Exception {
240+
final AtomicBoolean executed = new AtomicBoolean();
241+
final BaseRestHandler handler = new BaseRestHandler(Settings.EMPTY) {
242+
@Override
243+
protected RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException {
244+
request.content();
245+
return channel -> executed.set(true);
246+
}
247+
248+
@Override
249+
public String getName() {
250+
return "test_consumed_body";
251+
}
252+
253+
};
254+
255+
try (XContentBuilder builder = JsonXContent.contentBuilder().startObject().endObject()) {
256+
final RestRequest request = new FakeRestRequest.Builder(xContentRegistry())
257+
.withContent(new BytesArray(builder.toString()), XContentType.JSON)
258+
.build();
259+
final RestChannel channel = new FakeRestChannel(request, randomBoolean(), 1);
260+
handler.handleRequest(request, channel, mock(NodeClient.class));
261+
assertTrue(executed.get());
262+
}
263+
}
264+
265+
public void testUnconsumedNoBody() throws Exception {
266+
final AtomicBoolean executed = new AtomicBoolean();
267+
final BaseRestHandler handler = new BaseRestHandler(Settings.EMPTY) {
268+
@Override
269+
protected RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException {
270+
return channel -> executed.set(true);
271+
}
272+
273+
@Override
274+
public String getName() {
275+
return "test_unconsumed_body";
276+
}
277+
278+
};
279+
280+
final RestRequest request = new FakeRestRequest.Builder(xContentRegistry()).build();
281+
final RestChannel channel = new FakeRestChannel(request, randomBoolean(), 1);
282+
handler.handleRequest(request, channel, mock(NodeClient.class));
283+
assertTrue(executed.get());
284+
}
285+
286+
public void testUnconsumedBody() throws IOException {
287+
final AtomicBoolean executed = new AtomicBoolean();
288+
final BaseRestHandler handler = new BaseRestHandler(Settings.EMPTY) {
289+
@Override
290+
protected RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException {
291+
return channel -> executed.set(true);
292+
}
293+
294+
@Override
295+
public String getName() {
296+
return "test_unconsumed_body";
297+
}
298+
299+
};
300+
301+
try (XContentBuilder builder = JsonXContent.contentBuilder().startObject().endObject()) {
302+
final RestRequest request = new FakeRestRequest.Builder(xContentRegistry())
303+
.withContent(new BytesArray(builder.toString()), XContentType.JSON)
304+
.build();
305+
final RestChannel channel = new FakeRestChannel(request, randomBoolean(), 1);
306+
final IllegalArgumentException e =
307+
expectThrows(IllegalArgumentException.class, () -> handler.handleRequest(request, channel, mock(NodeClient.class)));
308+
assertThat(e, hasToString(containsString("request [GET /] does not support having a body")));
309+
assertFalse(executed.get());
310+
}
311+
}
312+
235313
}

server/src/test/java/org/elasticsearch/rest/RestRequestTests.java

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@
2020
package org.elasticsearch.rest;
2121

2222
import org.elasticsearch.ElasticsearchParseException;
23-
import org.elasticsearch.common.Strings;
23+
import org.elasticsearch.common.CheckedConsumer;
2424
import org.elasticsearch.common.bytes.BytesArray;
2525
import org.elasticsearch.common.bytes.BytesReference;
2626
import org.elasticsearch.common.collect.MapBuilder;
2727
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
28+
import org.elasticsearch.common.xcontent.XContentParser;
2829
import org.elasticsearch.common.xcontent.XContentType;
30+
import org.elasticsearch.http.HttpChannel;
31+
import org.elasticsearch.http.HttpRequest;
2932
import org.elasticsearch.test.ESTestCase;
3033
import org.elasticsearch.test.rest.FakeRestRequest;
3134

@@ -41,8 +44,62 @@
4144
import static java.util.Collections.singletonMap;
4245
import static org.hamcrest.Matchers.equalTo;
4346
import static org.hamcrest.Matchers.instanceOf;
47+
import static org.mockito.Mockito.mock;
48+
import static org.mockito.Mockito.when;
4449

4550
public class RestRequestTests extends ESTestCase {
51+
52+
public void testContentConsumesContent() {
53+
runConsumesContentTest(RestRequest::content, true);
54+
}
55+
56+
public void testRequiredContentConsumesContent() {
57+
runConsumesContentTest(RestRequest::requiredContent, true);
58+
}
59+
60+
public void testContentParserConsumesContent() {
61+
runConsumesContentTest(RestRequest::contentParser, true);
62+
}
63+
64+
public void testContentOrSourceParamConsumesContent() {
65+
runConsumesContentTest(RestRequest::contentOrSourceParam, true);
66+
}
67+
68+
public void testContentOrSourceParamsParserConsumesContent() {
69+
runConsumesContentTest(RestRequest::contentOrSourceParamParser, true);
70+
}
71+
72+
public void testWithContentOrSourceParamParserOrNullConsumesContent() {
73+
@SuppressWarnings("unchecked") CheckedConsumer<XContentParser, IOException> consumer = mock(CheckedConsumer.class);
74+
runConsumesContentTest(request -> request.withContentOrSourceParamParserOrNull(consumer), true);
75+
}
76+
77+
public void testApplyContentParserConsumesContent() {
78+
@SuppressWarnings("unchecked") CheckedConsumer<XContentParser, IOException> consumer = mock(CheckedConsumer.class);
79+
runConsumesContentTest(request -> request.applyContentParser(consumer), true);
80+
}
81+
82+
public void testHasContentDoesNotConsumesContent() {
83+
runConsumesContentTest(RestRequest::hasContent, false);
84+
}
85+
86+
private <T extends Exception> void runConsumesContentTest(
87+
final CheckedConsumer<RestRequest, T> consumer, final boolean expected) {
88+
final HttpRequest httpRequest = mock(HttpRequest.class);
89+
when (httpRequest.uri()).thenReturn("");
90+
when (httpRequest.content()).thenReturn(new BytesArray(new byte[1]));
91+
final RestRequest request =
92+
RestRequest.request(mock(NamedXContentRegistry.class), httpRequest, mock(HttpChannel.class));
93+
request.setXContentType(XContentType.JSON);
94+
assertFalse(request.isContentConsumed());
95+
try {
96+
consumer.accept(request);
97+
} catch (final Exception e) {
98+
throw new RuntimeException(e);
99+
}
100+
assertThat(request.isContentConsumed(), equalTo(expected));
101+
}
102+
46103
public void testContentParser() throws IOException {
47104
Exception e = expectThrows(ElasticsearchParseException.class, () ->
48105
contentRestRequest("", emptyMap()).contentParser());
@@ -211,14 +268,10 @@ public String uri() {
211268
return restRequest.uri();
212269
}
213270

214-
@Override
215-
public boolean hasContent() {
216-
return Strings.hasLength(content());
217-
}
218-
219271
@Override
220272
public BytesReference content() {
221273
return restRequest.content();
222274
}
223275
}
276+
224277
}

test/framework/src/main/java/org/elasticsearch/test/rest/FakeRestRequest.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,6 @@ private FakeRestRequest(NamedXContentRegistry xContentRegistry, HttpRequest http
4848
super(xContentRegistry, params, httpRequest.uri(), httpRequest.getHeaders(), httpRequest, httpChannel);
4949
}
5050

51-
@Override
52-
public boolean hasContent() {
53-
return content() != null;
54-
}
55-
5651
private static class FakeHttpRequest implements HttpRequest {
5752

5853
private final Method method;
@@ -166,7 +161,7 @@ public static class Builder {
166161

167162
private Map<String, String> params = new HashMap<>();
168163

169-
private BytesReference content;
164+
private BytesReference content = BytesArray.EMPTY;
170165

171166
private String path = "/";
172167

0 commit comments

Comments
 (0)