Skip to content

Commit d18fb8b

Browse files
Fitblipspinscale
authored andcommitted
REST API: Allow to configure JSONP/callback support
Added the http.jsonp.enable option to configure disabling of JSONP responses, as those might pose a security risk, and can be disabled if unused. This also fixes bugs in NettyHttpChannel * JSONP responses were never setting application/javascript as the content-type * The content-type and content-length headers were being overwritten even if they were set before Closes elastic#6164
1 parent 011e206 commit d18fb8b

File tree

6 files changed

+165
-4
lines changed

6 files changed

+165
-4
lines changed

config/elasticsearch.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,3 +375,11 @@
375375
#monitor.jvm.gc.old.warn: 10s
376376
#monitor.jvm.gc.old.info: 5s
377377
#monitor.jvm.gc.old.debug: 2s
378+
379+
################################## Security ################################
380+
381+
# Uncomment if you want to disable JSONP as a valid return transport on the
382+
# http server. With this enabled, it may pose a security risk, so disabling
383+
# it unless you need it is recommended.
384+
#
385+
#http.jsonp.enable: false

docs/reference/api-conventions.asciidoc

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,17 @@ document indexed.
241241
[float]
242242
=== JSONP
243243

244-
All REST APIs accept a `callback` parameter resulting in a
245-
http://en.wikipedia.org/wiki/JSONP[JSONP] result.
244+
By default JSONP resposes are enabled. All REST APIs accept a `callback` parameter
245+
resulting in a http://en.wikipedia.org/wiki/JSONP[JSONP] result. You can disable
246+
this behavior by adding the following to `config.yaml`:
247+
248+
http.jsonp.enable: false
249+
250+
Please note, due to the architecture of Elasticsearch, this may pose a security
251+
risk. Under some circumstances, an attacker may be able to exfiltrate data in your
252+
Elasticsearch server if they're able to force your browser to make a JSONP request
253+
on your behalf (e.g. by including a <script> tag on an untrusted site with a
254+
legitimate query against a local Elasticsearch server).
246255

247256
[float]
248257
=== Request body in query string

src/main/java/org/elasticsearch/http/netty/NettyHttpChannel.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,20 @@ public void sendResponse(RestResponse response) {
139139
buffer,
140140
ChannelBuffers.wrappedBuffer(END_JSONP)
141141
);
142+
// Add content-type header of "application/javascript"
143+
resp.headers().add(HttpHeaders.Names.CONTENT_TYPE, "application/javascript");
142144
}
143145
resp.setContent(buffer);
144-
resp.headers().add(HttpHeaders.Names.CONTENT_TYPE, response.contentType());
145-
resp.headers().add(HttpHeaders.Names.CONTENT_LENGTH, String.valueOf(buffer.readableBytes()));
146+
147+
// If our response doesn't specify a content-type header, set one
148+
if (!resp.headers().contains(HttpHeaders.Names.CONTENT_TYPE)) {
149+
resp.headers().add(HttpHeaders.Names.CONTENT_TYPE, response.contentType());
150+
}
151+
152+
// If our response has no content-length, calculate and set one
153+
if (!resp.headers().contains(HttpHeaders.Names.CONTENT_LENGTH)) {
154+
resp.headers().add(HttpHeaders.Names.CONTENT_LENGTH, String.valueOf(buffer.readableBytes()));
155+
}
146156

147157
if (transport.resetCookies) {
148158
String cookieString = nettyRequest.headers().get(HttpHeaders.Names.COOKIE);

src/main/java/org/elasticsearch/rest/RestController.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.elasticsearch.common.inject.Inject;
2828
import org.elasticsearch.common.path.PathTrie;
2929
import org.elasticsearch.common.settings.Settings;
30+
import org.elasticsearch.common.xcontent.XContentBuilder;
3031
import org.elasticsearch.rest.support.RestUtils;
3132

3233
import java.io.IOException;
@@ -35,6 +36,7 @@
3536

3637
import static org.elasticsearch.rest.RestStatus.BAD_REQUEST;
3738
import static org.elasticsearch.rest.RestStatus.OK;
39+
import static org.elasticsearch.rest.RestStatus.FORBIDDEN;
3840

3941
/**
4042
*
@@ -137,6 +139,20 @@ public RestFilterChain filterChain(RestFilter executionFilter) {
137139
}
138140

139141
public void dispatchRequest(final RestRequest request, final RestChannel channel) {
142+
// If JSONP is disabled and someone sends a callback parameter we should bail out before querying
143+
if (!settings.getAsBoolean("http.jsonp.enable", true) && request.hasParam("callback")){
144+
try {
145+
XContentBuilder builder = channel.newBuilder();
146+
builder.startObject().field("error","JSONP is disabled.").endObject().string();
147+
RestResponse response = new BytesRestResponse(FORBIDDEN, builder);
148+
response.addHeader("Content-Type", "application/javascript");
149+
channel.sendResponse(response);
150+
} catch (IOException e) {
151+
logger.warn("Failed to send response", e);
152+
return;
153+
}
154+
return;
155+
}
140156
if (filters.length == 0) {
141157
try {
142158
executeHandler(request, channel);
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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.options.jsonp;
21+
22+
import org.elasticsearch.common.settings.ImmutableSettings;
23+
import org.elasticsearch.common.settings.Settings;
24+
import org.elasticsearch.http.HttpServerTransport;
25+
import org.elasticsearch.rest.helper.HttpClient;
26+
import org.elasticsearch.rest.helper.HttpClientResponse;
27+
import org.elasticsearch.test.ElasticsearchIntegrationTest;
28+
import org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope;
29+
import org.elasticsearch.test.ElasticsearchIntegrationTest.Scope;
30+
import org.junit.Test;
31+
32+
import static org.hamcrest.Matchers.containsString;
33+
import static org.hamcrest.Matchers.is;
34+
35+
// Test to make sure that our JSONp response is disabled
36+
@ClusterScope(scope = Scope.TEST, numDataNodes = 1)
37+
public class JsonpOptionDisabledTest extends ElasticsearchIntegrationTest {
38+
39+
// Build our cluster settings
40+
@Override
41+
protected Settings nodeSettings(int nodeOrdinal) {
42+
return ImmutableSettings.settingsBuilder()
43+
.put("http.jsonp.enable", false)
44+
.put(super.nodeSettings(nodeOrdinal))
45+
.build();
46+
}
47+
48+
// Make sure our response has both the callback as well as our "JSONP is disabled" message.
49+
@Test
50+
public void testThatJSONPisDisabled() throws Exception {
51+
// Make the HTTP request
52+
HttpServerTransport httpServerTransport = internalCluster().getDataNodeInstance(HttpServerTransport.class);
53+
HttpClient httpClient = new HttpClient(httpServerTransport.boundAddress().publishAddress());
54+
HttpClientResponse response = httpClient.request("/?callback=DisabledJSONPCallback");
55+
assertThat(response.getHeader("Content-Type"), is("application/javascript"));
56+
assertThat(response.response(), containsString("DisabledJSONPCallback("));
57+
assertThat(response.response(), containsString("JSONP is disabled"));
58+
}
59+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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.options.jsonp;
21+
22+
import org.elasticsearch.common.settings.ImmutableSettings;
23+
import org.elasticsearch.common.settings.Settings;
24+
import org.elasticsearch.http.HttpServerTransport;
25+
import org.elasticsearch.rest.helper.HttpClient;
26+
import org.elasticsearch.rest.helper.HttpClientResponse;
27+
import org.elasticsearch.test.ElasticsearchIntegrationTest;
28+
import org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope;
29+
import org.elasticsearch.test.ElasticsearchIntegrationTest.Scope;
30+
import org.junit.Test;
31+
32+
import static org.hamcrest.Matchers.containsString;
33+
import static org.hamcrest.Matchers.is;
34+
35+
// Test to make sure that our JSONp response is enabled by default
36+
@ClusterScope(scope = Scope.TEST, numDataNodes = 1)
37+
public class JsonpOptionEnabledTest extends ElasticsearchIntegrationTest {
38+
39+
// Build our cluster settings
40+
@Override
41+
protected Settings nodeSettings(int nodeOrdinal) {
42+
return ImmutableSettings.settingsBuilder()
43+
.put("http.jsonp.enable", true)
44+
.put(super.nodeSettings(nodeOrdinal))
45+
.build();
46+
}
47+
48+
// Make sure our response has both the callback and opening paren, as well as the famous Elasticsearch tagline :)
49+
@Test
50+
public void testThatJSONPisEnabled() throws Exception {
51+
// Make the HTTP request
52+
HttpServerTransport httpServerTransport = internalCluster().getDataNodeInstance(HttpServerTransport.class);
53+
HttpClient httpClient = new HttpClient(httpServerTransport.boundAddress().publishAddress());
54+
HttpClientResponse response = httpClient.request("/?callback=EnabledJSONPCallback");
55+
assertThat(response.getHeader("Content-Type"), is("application/javascript"));
56+
assertThat(response.response(), containsString("EnabledJSONPCallback("));
57+
assertThat(response.response(), containsString("You Know, for Search"));
58+
}
59+
}

0 commit comments

Comments
 (0)