Browse Source

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 #6164
Fitblip 11 years ago
parent
commit
d18fb8bfbd

+ 8 - 0
config/elasticsearch.yml

@@ -375,3 +375,11 @@
 #monitor.jvm.gc.old.warn: 10s
 #monitor.jvm.gc.old.info: 5s
 #monitor.jvm.gc.old.debug: 2s
+
+################################## Security ################################
+
+# Uncomment if you want to disable JSONP as a valid return transport on the
+# http server. With this enabled, it may pose a security risk, so disabling
+# it unless you need it is recommended.
+#
+#http.jsonp.enable: false

+ 11 - 2
docs/reference/api-conventions.asciidoc

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

+ 12 - 2
src/main/java/org/elasticsearch/http/netty/NettyHttpChannel.java

@@ -139,10 +139,20 @@ public class NettyHttpChannel extends HttpChannel {
                         buffer,
                         ChannelBuffers.wrappedBuffer(END_JSONP)
                 );
+                // Add content-type header of "application/javascript"
+                resp.headers().add(HttpHeaders.Names.CONTENT_TYPE, "application/javascript");
             }
             resp.setContent(buffer);
-            resp.headers().add(HttpHeaders.Names.CONTENT_TYPE, response.contentType());
-            resp.headers().add(HttpHeaders.Names.CONTENT_LENGTH, String.valueOf(buffer.readableBytes()));
+
+            // If our response doesn't specify a content-type header, set one
+            if (!resp.headers().contains(HttpHeaders.Names.CONTENT_TYPE)) {
+                resp.headers().add(HttpHeaders.Names.CONTENT_TYPE, response.contentType());
+            }
+
+            // If our response has no content-length, calculate and set one
+            if (!resp.headers().contains(HttpHeaders.Names.CONTENT_LENGTH)) {
+                resp.headers().add(HttpHeaders.Names.CONTENT_LENGTH, String.valueOf(buffer.readableBytes()));
+            }
 
             if (transport.resetCookies) {
                 String cookieString = nettyRequest.headers().get(HttpHeaders.Names.COOKIE);

+ 16 - 0
src/main/java/org/elasticsearch/rest/RestController.java

@@ -27,6 +27,7 @@ import org.elasticsearch.common.component.AbstractLifecycleComponent;
 import org.elasticsearch.common.inject.Inject;
 import org.elasticsearch.common.path.PathTrie;
 import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.rest.support.RestUtils;
 
 import java.io.IOException;
@@ -35,6 +36,7 @@ import java.util.Comparator;
 
 import static org.elasticsearch.rest.RestStatus.BAD_REQUEST;
 import static org.elasticsearch.rest.RestStatus.OK;
+import static org.elasticsearch.rest.RestStatus.FORBIDDEN;
 
 /**
  *
@@ -137,6 +139,20 @@ public class RestController extends AbstractLifecycleComponent<RestController> {
     }
 
     public void dispatchRequest(final RestRequest request, final RestChannel channel) {
+        // If JSONP is disabled and someone sends a callback parameter we should bail out before querying
+        if (!settings.getAsBoolean("http.jsonp.enable", true) && request.hasParam("callback")){
+            try {
+                XContentBuilder builder = channel.newBuilder();
+                builder.startObject().field("error","JSONP is disabled.").endObject().string();
+                RestResponse response = new BytesRestResponse(FORBIDDEN, builder);
+                response.addHeader("Content-Type", "application/javascript");
+                channel.sendResponse(response);
+            } catch (IOException e) {
+                logger.warn("Failed to send response", e);
+                return;
+            }
+            return;
+        }
         if (filters.length == 0) {
             try {
                 executeHandler(request, channel);

+ 59 - 0
src/test/java/org/elasticsearch/options/jsonp/JsonpOptionDisabledTest.java

@@ -0,0 +1,59 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.options.jsonp;
+
+import org.elasticsearch.common.settings.ImmutableSettings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.http.HttpServerTransport;
+import org.elasticsearch.rest.helper.HttpClient;
+import org.elasticsearch.rest.helper.HttpClientResponse;
+import org.elasticsearch.test.ElasticsearchIntegrationTest;
+import org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope;
+import org.elasticsearch.test.ElasticsearchIntegrationTest.Scope;
+import org.junit.Test;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+
+// Test to make sure that our JSONp response is disabled
+@ClusterScope(scope = Scope.TEST, numDataNodes = 1)
+public class JsonpOptionDisabledTest extends ElasticsearchIntegrationTest {
+
+    // Build our cluster settings
+    @Override
+    protected Settings nodeSettings(int nodeOrdinal) {
+        return ImmutableSettings.settingsBuilder()
+                .put("http.jsonp.enable", false)
+                .put(super.nodeSettings(nodeOrdinal))
+                .build();
+    }
+
+    // Make sure our response has both the callback as well as our "JSONP is disabled" message. 
+    @Test
+    public void testThatJSONPisDisabled() throws Exception {
+        // Make the HTTP request
+        HttpServerTransport httpServerTransport = internalCluster().getDataNodeInstance(HttpServerTransport.class);
+        HttpClient httpClient = new HttpClient(httpServerTransport.boundAddress().publishAddress());
+        HttpClientResponse response = httpClient.request("/?callback=DisabledJSONPCallback");
+        assertThat(response.getHeader("Content-Type"), is("application/javascript"));
+        assertThat(response.response(), containsString("DisabledJSONPCallback("));
+        assertThat(response.response(), containsString("JSONP is disabled"));
+    }
+}

+ 59 - 0
src/test/java/org/elasticsearch/options/jsonp/JsonpOptionEnabledTest.java

@@ -0,0 +1,59 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.options.jsonp;
+
+import org.elasticsearch.common.settings.ImmutableSettings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.http.HttpServerTransport;
+import org.elasticsearch.rest.helper.HttpClient;
+import org.elasticsearch.rest.helper.HttpClientResponse;
+import org.elasticsearch.test.ElasticsearchIntegrationTest;
+import org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope;
+import org.elasticsearch.test.ElasticsearchIntegrationTest.Scope;
+import org.junit.Test;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+
+// Test to make sure that our JSONp response is enabled by default
+@ClusterScope(scope = Scope.TEST, numDataNodes = 1)
+public class JsonpOptionEnabledTest extends ElasticsearchIntegrationTest {
+
+    // Build our cluster settings
+    @Override
+    protected Settings nodeSettings(int nodeOrdinal) {
+        return ImmutableSettings.settingsBuilder()
+                .put("http.jsonp.enable", true)
+                .put(super.nodeSettings(nodeOrdinal))
+                .build();
+    }
+
+    // Make sure our response has both the callback and opening paren, as well as the famous Elasticsearch tagline :)
+    @Test
+    public void testThatJSONPisEnabled() throws Exception {
+        // Make the HTTP request
+        HttpServerTransport httpServerTransport = internalCluster().getDataNodeInstance(HttpServerTransport.class);
+        HttpClient httpClient = new HttpClient(httpServerTransport.boundAddress().publishAddress());
+        HttpClientResponse response = httpClient.request("/?callback=EnabledJSONPCallback");
+        assertThat(response.getHeader("Content-Type"), is("application/javascript"));
+        assertThat(response.response(), containsString("EnabledJSONPCallback("));
+        assertThat(response.response(), containsString("You Know, for Search"));
+    }
+}