Skip to content

Commit 4ef3de4

Browse files
authored
Fix handling of bad requests (elastic#29249)
Today we have a few problems with how we handle bad requests: - handling requests with bad encoding - handling requests with invalid value for filter_path/pretty/human - handling requests with a garbage Content-Type header There are two problems: - in every case, we give an empty response to the client - in most cases, we leak the byte buffer backing the request! These problems are caused by a broader problem: poor handling preparing the request for handling, or the channel to write to when the response is ready. This commit addresses these issues by taking a unified approach to all of them that ensures that: - we respond to the client with the exception that blew us up - we do not leak the byte buffer backing the request
1 parent 13e19e7 commit 4ef3de4

File tree

11 files changed

+398
-84
lines changed

11 files changed

+398
-84
lines changed

modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpRequest.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import io.netty.handler.codec.http.FullHttpRequest;
2424
import io.netty.handler.codec.http.HttpHeaders;
2525
import io.netty.handler.codec.http.HttpMethod;
26-
2726
import org.elasticsearch.common.bytes.BytesArray;
2827
import org.elasticsearch.common.bytes.BytesReference;
2928
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
@@ -45,6 +44,15 @@ public class Netty4HttpRequest extends RestRequest {
4544
private final Channel channel;
4645
private final BytesReference content;
4746

47+
/**
48+
* Construct a new request.
49+
*
50+
* @param xContentRegistry the content registry
51+
* @param request the underlying request
52+
* @param channel the channel for the request
53+
* @throws BadParameterException if the parameters can not be decoded
54+
* @throws ContentTypeHeaderException if the Content-Type header can not be parsed
55+
*/
4856
Netty4HttpRequest(NamedXContentRegistry xContentRegistry, FullHttpRequest request, Channel channel) {
4957
super(xContentRegistry, request.uri(), new HttpHeadersMap(request.headers()));
5058
this.request = request;
@@ -56,6 +64,34 @@ public class Netty4HttpRequest extends RestRequest {
5664
}
5765
}
5866

67+
/**
68+
* Construct a new request. In contrast to
69+
* {@link Netty4HttpRequest#Netty4HttpRequest(NamedXContentRegistry, Map, String, FullHttpRequest, Channel)}, the URI is not decoded so
70+
* this constructor will not throw a {@link BadParameterException}.
71+
*
72+
* @param xContentRegistry the content registry
73+
* @param params the parameters for the request
74+
* @param uri the path for the request
75+
* @param request the underlying request
76+
* @param channel the channel for the request
77+
* @throws ContentTypeHeaderException if the Content-Type header can not be parsed
78+
*/
79+
Netty4HttpRequest(
80+
final NamedXContentRegistry xContentRegistry,
81+
final Map<String, String> params,
82+
final String uri,
83+
final FullHttpRequest request,
84+
final Channel channel) {
85+
super(xContentRegistry, params, uri, new HttpHeadersMap(request.headers()));
86+
this.request = request;
87+
this.channel = channel;
88+
if (request.content().isReadable()) {
89+
this.content = Netty4Utils.toBytesReference(request.content());
90+
} else {
91+
this.content = BytesArray.EMPTY;
92+
}
93+
}
94+
5995
public FullHttpRequest request() {
6096
return this.request;
6197
}

modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpRequestHandler.java

Lines changed: 106 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,21 @@
2020
package org.elasticsearch.http.netty4;
2121

2222
import io.netty.buffer.Unpooled;
23+
import io.netty.channel.Channel;
2324
import io.netty.channel.ChannelHandler;
2425
import io.netty.channel.ChannelHandlerContext;
2526
import io.netty.channel.SimpleChannelInboundHandler;
2627
import io.netty.handler.codec.http.DefaultFullHttpRequest;
28+
import io.netty.handler.codec.http.DefaultHttpHeaders;
2729
import io.netty.handler.codec.http.FullHttpRequest;
30+
import io.netty.handler.codec.http.HttpHeaders;
2831
import org.elasticsearch.common.util.concurrent.ThreadContext;
2932
import org.elasticsearch.http.netty4.pipelining.HttpPipelinedRequest;
33+
import org.elasticsearch.rest.RestRequest;
3034
import org.elasticsearch.transport.netty4.Netty4Utils;
3135

36+
import java.util.Collections;
37+
3238
@ChannelHandler.Sharable
3339
class Netty4HttpRequestHandler extends SimpleChannelInboundHandler<Object> {
3440

@@ -56,32 +62,113 @@ protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Except
5662
request = (FullHttpRequest) msg;
5763
}
5864

59-
final FullHttpRequest copy =
65+
boolean success = false;
66+
try {
67+
68+
final FullHttpRequest copy =
69+
new DefaultFullHttpRequest(
70+
request.protocolVersion(),
71+
request.method(),
72+
request.uri(),
73+
Unpooled.copiedBuffer(request.content()),
74+
request.headers(),
75+
request.trailingHeaders());
76+
77+
Exception badRequestCause = null;
78+
79+
/*
80+
* We want to create a REST request from the incoming request from Netty. However, creating this request could fail if there
81+
* are incorrectly encoded parameters, or the Content-Type header is invalid. If one of these specific failures occurs, we
82+
* attempt to create a REST request again without the input that caused the exception (e.g., we remove the Content-Type header,
83+
* or skip decoding the parameters). Once we have a request in hand, we then dispatch the request as a bad request with the
84+
* underlying exception that caused us to treat the request as bad.
85+
*/
86+
final Netty4HttpRequest httpRequest;
87+
{
88+
Netty4HttpRequest innerHttpRequest;
89+
try {
90+
innerHttpRequest = new Netty4HttpRequest(serverTransport.xContentRegistry, copy, ctx.channel());
91+
} catch (final RestRequest.ContentTypeHeaderException e) {
92+
badRequestCause = e;
93+
innerHttpRequest = requestWithoutContentTypeHeader(copy, ctx.channel(), badRequestCause);
94+
} catch (final RestRequest.BadParameterException e) {
95+
badRequestCause = e;
96+
innerHttpRequest = requestWithoutParameters(copy, ctx.channel());
97+
}
98+
httpRequest = innerHttpRequest;
99+
}
100+
101+
/*
102+
* We now want to create a channel used to send the response on. However, creating this channel can fail if there are invalid
103+
* parameter values for any of the filter_path, human, or pretty parameters. We detect these specific failures via an
104+
* IllegalArgumentException from the channel constructor and then attempt to create a new channel that bypasses parsing of these
105+
* parameter values.
106+
*/
107+
final Netty4HttpChannel channel;
108+
{
109+
Netty4HttpChannel innerChannel;
110+
try {
111+
innerChannel =
112+
new Netty4HttpChannel(serverTransport, httpRequest, pipelinedRequest, detailedErrorsEnabled, threadContext);
113+
} catch (final IllegalArgumentException e) {
114+
if (badRequestCause == null) {
115+
badRequestCause = e;
116+
} else {
117+
badRequestCause.addSuppressed(e);
118+
}
119+
final Netty4HttpRequest innerRequest =
120+
new Netty4HttpRequest(
121+
serverTransport.xContentRegistry,
122+
Collections.emptyMap(), // we are going to dispatch the request as a bad request, drop all parameters
123+
copy.uri(),
124+
copy,
125+
ctx.channel());
126+
innerChannel =
127+
new Netty4HttpChannel(serverTransport, innerRequest, pipelinedRequest, detailedErrorsEnabled, threadContext);
128+
}
129+
channel = innerChannel;
130+
}
131+
132+
if (request.decoderResult().isFailure()) {
133+
serverTransport.dispatchBadRequest(httpRequest, channel, request.decoderResult().cause());
134+
} else if (badRequestCause != null) {
135+
serverTransport.dispatchBadRequest(httpRequest, channel, badRequestCause);
136+
} else {
137+
serverTransport.dispatchRequest(httpRequest, channel);
138+
}
139+
success = true;
140+
} finally {
141+
// the request is otherwise released in case of dispatch
142+
if (success == false && pipelinedRequest != null) {
143+
pipelinedRequest.release();
144+
}
145+
}
146+
}
147+
148+
private Netty4HttpRequest requestWithoutContentTypeHeader(
149+
final FullHttpRequest request, final Channel channel, final Exception badRequestCause) {
150+
final HttpHeaders headersWithoutContentTypeHeader = new DefaultHttpHeaders();
151+
headersWithoutContentTypeHeader.add(request.headers());
152+
headersWithoutContentTypeHeader.remove("Content-Type");
153+
final FullHttpRequest requestWithoutContentTypeHeader =
60154
new DefaultFullHttpRequest(
61155
request.protocolVersion(),
62156
request.method(),
63157
request.uri(),
64-
Unpooled.copiedBuffer(request.content()),
65-
request.headers(),
66-
request.trailingHeaders());
67-
final Netty4HttpRequest httpRequest;
158+
request.content(),
159+
headersWithoutContentTypeHeader, // remove the Content-Type header so as to not parse it again
160+
request.trailingHeaders()); // Content-Type can not be a trailing header
68161
try {
69-
httpRequest = new Netty4HttpRequest(serverTransport.xContentRegistry, copy, ctx.channel());
70-
} catch (Exception ex) {
71-
if (pipelinedRequest != null) {
72-
pipelinedRequest.release();
73-
}
74-
throw ex;
162+
return new Netty4HttpRequest(serverTransport.xContentRegistry, requestWithoutContentTypeHeader, channel);
163+
} catch (final RestRequest.BadParameterException e) {
164+
badRequestCause.addSuppressed(e);
165+
return requestWithoutParameters(requestWithoutContentTypeHeader, channel);
75166
}
76-
final Netty4HttpChannel channel =
77-
new Netty4HttpChannel(serverTransport, httpRequest, pipelinedRequest, detailedErrorsEnabled, threadContext);
167+
}
78168

79-
if (request.decoderResult().isSuccess()) {
80-
serverTransport.dispatchRequest(httpRequest, channel);
81-
} else {
82-
assert request.decoderResult().isFailure();
83-
serverTransport.dispatchBadRequest(httpRequest, channel, request.decoderResult().cause());
84-
}
169+
private Netty4HttpRequest requestWithoutParameters(final FullHttpRequest request, final Channel channel) {
170+
// remove all parameters as at least one is incorrectly encoded
171+
return new Netty4HttpRequest(serverTransport.xContentRegistry, Collections.emptyMap(), request.uri(), request, channel);
85172
}
86173

87174
@Override
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.http.netty4;
21+
22+
import io.netty.handler.codec.http.FullHttpResponse;
23+
import org.elasticsearch.ElasticsearchException;
24+
import org.elasticsearch.common.network.NetworkService;
25+
import org.elasticsearch.common.settings.Settings;
26+
import org.elasticsearch.common.transport.TransportAddress;
27+
import org.elasticsearch.common.util.MockBigArrays;
28+
import org.elasticsearch.common.util.MockPageCacheRecycler;
29+
import org.elasticsearch.common.util.concurrent.ThreadContext;
30+
import org.elasticsearch.http.HttpServerTransport;
31+
import org.elasticsearch.indices.breaker.NoneCircuitBreakerService;
32+
import org.elasticsearch.rest.BytesRestResponse;
33+
import org.elasticsearch.rest.RestChannel;
34+
import org.elasticsearch.rest.RestRequest;
35+
import org.elasticsearch.rest.RestStatus;
36+
import org.elasticsearch.test.ESTestCase;
37+
import org.elasticsearch.threadpool.TestThreadPool;
38+
import org.elasticsearch.threadpool.ThreadPool;
39+
import org.junit.After;
40+
import org.junit.Before;
41+
42+
import java.io.IOException;
43+
import java.io.UncheckedIOException;
44+
import java.util.Collection;
45+
import java.util.Collections;
46+
47+
import static org.hamcrest.Matchers.containsString;
48+
import static org.hamcrest.Matchers.equalTo;
49+
import static org.hamcrest.Matchers.hasSize;
50+
51+
public class Netty4BadRequestTests extends ESTestCase {
52+
53+
private NetworkService networkService;
54+
private MockBigArrays bigArrays;
55+
private ThreadPool threadPool;
56+
57+
@Before
58+
public void setup() throws Exception {
59+
networkService = new NetworkService(Collections.emptyList());
60+
bigArrays = new MockBigArrays(new MockPageCacheRecycler(Settings.EMPTY), new NoneCircuitBreakerService());
61+
threadPool = new TestThreadPool("test");
62+
}
63+
64+
@After
65+
public void shutdown() throws Exception {
66+
terminate(threadPool);
67+
}
68+
69+
public void testBadParameterEncoding() throws Exception {
70+
final HttpServerTransport.Dispatcher dispatcher = new HttpServerTransport.Dispatcher() {
71+
@Override
72+
public void dispatchRequest(RestRequest request, RestChannel channel, ThreadContext threadContext) {
73+
fail();
74+
}
75+
76+
@Override
77+
public void dispatchBadRequest(RestRequest request, RestChannel channel, ThreadContext threadContext, Throwable cause) {
78+
try {
79+
final Exception e = cause instanceof Exception ? (Exception) cause : new ElasticsearchException(cause);
80+
channel.sendResponse(new BytesRestResponse(channel, RestStatus.BAD_REQUEST, e));
81+
} catch (final IOException e) {
82+
throw new UncheckedIOException(e);
83+
}
84+
}
85+
};
86+
87+
try (HttpServerTransport httpServerTransport =
88+
new Netty4HttpServerTransport(Settings.EMPTY, networkService, bigArrays, threadPool, xContentRegistry(), dispatcher)) {
89+
httpServerTransport.start();
90+
final TransportAddress transportAddress = randomFrom(httpServerTransport.boundAddress().boundAddresses());
91+
92+
try (Netty4HttpClient nettyHttpClient = new Netty4HttpClient()) {
93+
final Collection<FullHttpResponse> responses =
94+
nettyHttpClient.get(transportAddress.address(), "/_cluster/settings?pretty=%");
95+
assertThat(responses, hasSize(1));
96+
assertThat(responses.iterator().next().status().code(), equalTo(400));
97+
final Collection<String> responseBodies = Netty4HttpClient.returnHttpResponseBodies(responses);
98+
assertThat(responseBodies, hasSize(1));
99+
assertThat(responseBodies.iterator().next(), containsString("\"type\":\"bad_parameter_exception\""));
100+
assertThat(
101+
responseBodies.iterator().next(),
102+
containsString(
103+
"\"reason\":\"java.lang.IllegalArgumentException: unterminated escape sequence at end of string: %\""));
104+
}
105+
}
106+
}
107+
108+
}

modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpChannelTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,8 @@ private FullHttpResponse executeRequest(final Settings settings, final String or
330330
}
331331
httpRequest.headers().add(HttpHeaderNames.HOST, host);
332332
final WriteCapturingChannel writeCapturingChannel = new WriteCapturingChannel();
333-
final Netty4HttpRequest request = new Netty4HttpRequest(xContentRegistry(), httpRequest, writeCapturingChannel);
333+
final Netty4HttpRequest request =
334+
new Netty4HttpRequest(xContentRegistry(), httpRequest, writeCapturingChannel);
334335

335336
Netty4HttpChannel channel =
336337
new Netty4HttpChannel(httpServerTransport, request, null, randomBoolean(), threadPool.getThreadContext());

modules/transport-netty4/src/test/java/org/elasticsearch/rest/Netty4BadRequestIT.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
package org.elasticsearch.rest;
2121

22+
import org.apache.http.message.BasicHeader;
2223
import org.elasticsearch.client.Response;
2324
import org.elasticsearch.client.ResponseException;
2425
import org.elasticsearch.common.settings.Setting;
@@ -74,4 +75,29 @@ public void testBadRequest() throws IOException {
7475
assertThat(e, hasToString(containsString("too_long_frame_exception")));
7576
assertThat(e, hasToString(matches("An HTTP line is larger than \\d+ bytes")));
7677
}
78+
79+
public void testInvalidParameterValue() throws IOException {
80+
final ResponseException e = expectThrows(
81+
ResponseException.class,
82+
() -> client().performRequest("GET", "/_cluster/settings", Collections.singletonMap("pretty", "neither-true-nor-false")));
83+
final Response response = e.getResponse();
84+
assertThat(response.getStatusLine().getStatusCode(), equalTo(400));
85+
final ObjectPath objectPath = ObjectPath.createFromResponse(response);
86+
final Map<String, Object> map = objectPath.evaluate("error");
87+
assertThat(map.get("type"), equalTo("illegal_argument_exception"));
88+
assertThat(map.get("reason"), equalTo("Failed to parse value [neither-true-nor-false] as only [true] or [false] are allowed."));
89+
}
90+
91+
public void testInvalidHeaderValue() throws IOException {
92+
final BasicHeader header = new BasicHeader("Content-Type", "\t");
93+
final ResponseException e =
94+
expectThrows(ResponseException.class, () -> client().performRequest("GET", "/_cluster/settings", header));
95+
final Response response = e.getResponse();
96+
assertThat(response.getStatusLine().getStatusCode(), equalTo(400));
97+
final ObjectPath objectPath = ObjectPath.createFromResponse(response);
98+
final Map<String, Object> map = objectPath.evaluate("error");
99+
assertThat(map.get("type"), equalTo("content_type_header_exception"));
100+
assertThat(map.get("reason"), equalTo("java.lang.IllegalArgumentException: invalid Content-Type header []"));
101+
}
102+
77103
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ public abstract class AbstractRestChannel implements RestChannel {
4848

4949
private BytesStreamOutput bytesOut;
5050

51+
/**
52+
* Construct a channel for handling the request.
53+
*
54+
* @param request the request
55+
* @param detailedErrorsEnabled if detailed errors should be reported to the channel
56+
* @throws IllegalArgumentException if parsing the pretty or human parameters fails
57+
*/
5158
protected AbstractRestChannel(RestRequest request, boolean detailedErrorsEnabled) {
5259
this.request = request;
5360
this.detailedErrorsEnabled = detailedErrorsEnabled;

0 commit comments

Comments
 (0)