Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Security: Make JSONP responses optional. #6164

Closed
wants to merge 1 commit into from

Conversation

Fitblip
Copy link
Contributor

@Fitblip Fitblip commented May 14, 2014

This poses a significant security threat by being on by default. If an attacker can entice a user to load a legitimate ElasticSearch query as a script tag, they can effectively bypass SOP and exfiltrate the contents of a local ElasticSearch instance (or bypass firewall rules by using a victim's browser to talk to a remote instance).

This would be trivial to implement from an attackers perspective, and I'll be submitting a pull request to the BeEF project (https://github.com/beefproject/beef) to automate this sort of attack, as it'll be useful during a pentesting engagement.

If you have any questions or change requests please let me know, and I'll be more than happy to accommodate.

Thanks!
Fitblip

@Fitblip
Copy link
Contributor Author

Fitblip commented May 14, 2014

Crap, didn't mean to include the first commit. If you want me to remove it and re-submit my pull request, please let me know.

@Fitblip Fitblip changed the title Make JSONP responses optional. [Security] Make JSONP responses optional. May 14, 2014
@kimchy
Copy link
Member

kimchy commented May 28, 2014

I like this, and we were discussing internally to potentially disable this by default for 1.3, but at the very least people should be able to disable it. Few comments:

  • Can you remove the CORS comment? We have an idea on how to potentially solve this cleanly while still retaining out of the box experience, we will open an issue soon about it, it will be addressed in 1.3
  • For now, can you default it to true, and remove setting it in the settings, and lets open a different issue to discuss its default value?

@spinscale spinscale self-assigned this May 28, 2014
@Fitblip
Copy link
Contributor Author

Fitblip commented May 29, 2014

Agreed, thanks for the comments and insight, I'll work on that getting
implemented (hopefully) tomorrow and send an updated pull request. :)

As far as keeping it on by default, that's totally fair, I didn't think
about breaking backwards compatibility when I submitted this. We just
pushed to remediate this issue within our environment and noticed there
wasn't even a config directive for it!

What is JSONP usually used for? Kibana?

Fitblip

On Wed, May 28, 2014 at 6:16 AM, Shay Banon notifications@github.comwrote:

I like this, and we were discussing internally to potentially disable this
by default for 1.3, but at the very least people should be able to disable
it. Few comments:

  • Can you remove the CORS comment? We have an idea on how to
    potentially solve this cleanly while still retaining out of the box
    experience, we will open an issue soon about it, it will be addressed in 1.3
  • For now, can you default it to true, and remove setting it in the
    settings, and lets open a different issue to discuss its default value?


Reply to this email directly or view it on GitHubhttps://github.com//pull/6164#issuecomment-44404498
.

@Fitblip
Copy link
Contributor Author

Fitblip commented May 29, 2014

Hey @kimchy,

I've implemented your requested changes. Does everything look good?

Also if you guys would like to discuss JSONP, CORS, or really anything security related, please feel free to reach out to me. I use elasticsearch enough that I'm happy to give back in whatever form I can.

Fitblip

@kimchy
Copy link
Member

kimchy commented May 29, 2014

@Fitblip thanks!, will definitely keep you in the loop and @ you on the issue we open. @spinscale looks good to me, what do you think?

@Fitblip
Copy link
Contributor Author

Fitblip commented May 30, 2014

Awesome! Glad to help in any way I can 👍


################################## Security ################################

# If you want to enable JSONP as a valid return transport on the http server.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nitpick 1: as you changed the default behaviour, can you mention the disabling here explicitely and set the option to false below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good call, I'll fix that!

@spinscale
Copy link
Contributor

This looks great. left very minor comments. I'd like to include it, and just saw that you haven't signed the CLA. As soon as you do that (under http://www.elasticsearch.org/contributor-agreement/), I am happy to get this PR in!

@Fitblip
Copy link
Contributor Author

Fitblip commented May 30, 2014

Hey @spinscale,

I think I've implemented the changes you requested (I had some confusion on the first one), and signed the CLA.

Let me know if you need anything else from me 😃

@spinscale
Copy link
Contributor

Hey there,

I have been thinking about this a bit, and have another question about this impl. If you disable sending of JSONP, do you really want to execute the search request and return the results without a callback or do you want to return an error message telling the user that JSONP is disabled (and also not wasting precious process time executing a query).

What do you think makes more sense? I am leaning towards the option of returning an error, but maybe this is not considered best practice?

@Fitblip
Copy link
Contributor Author

Fitblip commented Jun 2, 2014

Hey @spinscale,

That's a good point actually. Returning a JSONP-formatted error response probably makes the most sense.

I can re-factor this to make that happen, but it may be a couple days.

Thanks!

@Fitblip
Copy link
Contributor Author

Fitblip commented Jun 3, 2014

@spinscale

This should throw an error message when JSONP is disabled, but I'm not sure where the actually query is getting executed, so I 't know if by not actually passing back the buffer it's never read from/executed or what. By the time NettyHttpChannel.sendResponse() is invoked is the query already run?

Or is this where it's executed?
https://github.com/Fitblip/elasticsearch/blob/master/src/main/java/org/elasticsearch/http/netty/NettyHttpChannel.java#L128

@spinscale
Copy link
Contributor

@Fitblip yeah the request has already been executed by then, if you have a RestResponse object

I guess we need to take a look at HttpServer.dispatchRequest() or RestController.dispatchRequest() and reject the request, before its being parsed.

I realize that you already put a considerable amount of time into this one, so I can just check by myself if you want, but I am as happy to help and assist, if you want to move forward (thanks a lot for all your work so far!)

Also, we need a test here, when we are clear about the actual impl, to make sure, the setting works as expected :-)

@Fitblip
Copy link
Contributor Author

Fitblip commented Jun 3, 2014

Awesome! Thanks for the info. For the most recent push do you have any preferences as to the parameters passed back through JSONP? Right now it's just error and status of 501 (HTTP not implemented), though we can either change that or remove it entirely. Up to you guys.

I can certainly take a stab at checking the parameter and bailing out. We can hammer out impl details once I have something to propose :).

I'll also be happy to write the test/s.

@Fitblip
Copy link
Contributor Author

Fitblip commented Jun 4, 2014

Hey @spinscale,

It looks like RestController.dispatchRequest is the place to do it (to me). I've essentially reverted all my changes to NettyHttpChannel.java, and implemented something that should work in RestController.java. I don't have time to test it tonight, but does my logic look sound to you? Anything else I should be concerned about?

I'll make sure things are working as expected tomorrow (not sure if I'm calling settings the right way), and fix any issues that crop up.

Thanks!

@spinscale
Copy link
Contributor

looks good (sorry, travelling the rest of the week, so I dont have too much time to comment)

@Fitblip
Copy link
Contributor Author

Fitblip commented Jun 4, 2014

No worries @spinscale. This should be good to go (minus the test). I've tested it on my local machine and it appears to work as expected.

I'll work on getting the test done, but I'm not so familiar with java tests (I mostly come from a python background), but I'm sure I'll figure something out.

@spinscale
Copy link
Contributor

hey,

I took a look at this and like it so far, I also have a test ready, so if you are ok with this, I take your work, add a test and push it.

@Fitblip
Copy link
Contributor Author

Fitblip commented Jun 10, 2014

Awesome! I guess we were both working on tests at the same time :).

If you don't mind, I'd like to get some help figuring out where my tests could be improved, and push this whole thing in together. That way I can actually write quality tests moving forward! If you prefer email communication you can reach me at my github username @gmail.com

XContentBuilder builder = channel.newBuilder();
String errorJSON = builder.startObject().field("error","JSONP is disabled. Set http.jsonp.enable = true to allow JSONP responses.").endObject().string();
channel.sendResponse(new BytesRestResponse(FORBIDDEN,"application/javascript",errorJSON));
} catch (IOException e) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you do not supply the the content-type, you can just hand over the builder and do not need to create a string object here.

Also I would just return JSON is disabled instead of mentioning the config option here. The shorter the better IMO.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair, but standards dictate that all JSONP responses should (at least in theory) return an application/javascript mime type.

http://stackoverflow.com/questions/111302/best-content-type-to-serve-jsonp

As for the note, I'll change it. I agree, it's a bit long-winded :)

@spinscale
Copy link
Contributor

actually my test looks nearly the same, nice work! Just had some minor nitpicks. Apart from that we are good and almost ready to go! Maybe you can squash the commits and force push so we only have one history commit, when getting it into ES.

@Fitblip
Copy link
Contributor Author

Fitblip commented Jun 11, 2014

Cool - thanks a ton for your help in all this @spinscale. One last thing, I notice that my disabled test seems to fail. Any idea why? It could very well be my eclipse env, but it seems like I should release something or call some sort of finished routine. For my JsonpOptionEnabledTest things are all green, but not for the disabled test. Traceback inline.

[2014-06-10 22:52:32,915][ERROR][org.elasticsearch.test   ] FAILURE  : testThatJSONPisDisabled(org.elasticsearch.options.jsonp.JsonpOptionDisabledTest)
REPRODUCE WITH  : mvn test -Dtests.seed=95F2428CAE02E7BE -Dtests.class=org.elasticsearch.options.jsonp.JsonpOptionDisabledTest -Dtests.prefix=tests -Dfile.encoding=UTF-8 -Duser.timezone=America/Los_Angeles -Dtests.method="testThatJSONPisDisabled"
Throwable:
java.lang.RuntimeException: 1 arrays have not been released
    org.elasticsearch.test.cache.recycler.MockBigArrays.ensureAllArraysAreReleased(MockBigArrays.java:68)
    org.elasticsearch.test.ElasticsearchTestCase.ensureAllArraysReleased(ElasticsearchTestCase.java:135)
    [...sun.*, com.carrotsearch.randomizedtesting.*, java.lang.reflect.*]
    org.apache.lucene.util.TestRuleSetupTeardownChained$1.evaluate(TestRuleSetupTeardownChained.java:50)
    org.apache.lucene.util.TestRuleFieldCacheSanity$1.evaluate(TestRuleFieldCacheSanity.java:51)
    org.apache.lucene.util.AbstractBeforeAfterRule$1.evaluate(AbstractBeforeAfterRule.java:46)
    [...com.carrotsearch.randomizedtesting.*]
    org.apache.lucene.util.TestRuleThreadAndTestName$1.evaluate(TestRuleThreadAndTestName.java:49)
    org.apache.lucene.util.TestRuleIgnoreAfterMaxFailures$1.evaluate(TestRuleIgnoreAfterMaxFailures.java:65)
    org.apache.lucene.util.TestRuleMarkFailure$1.evaluate(TestRuleMarkFailure.java:48)
    [...com.carrotsearch.randomizedtesting.*]
    org.apache.lucene.util.AbstractBeforeAfterRule$1.evaluate(AbstractBeforeAfterRule.java:46)
    org.apache.lucene.util.TestRuleStoreClassName$1.evaluate(TestRuleStoreClassName.java:42)
    [...com.carrotsearch.randomizedtesting.*]
    org.apache.lucene.util.TestRuleAssertionsRequired$1.evaluate(TestRuleAssertionsRequired.java:43)
    org.apache.lucene.util.TestRuleMarkFailure$1.evaluate(TestRuleMarkFailure.java:48)
    org.apache.lucene.util.TestRuleIgnoreAfterMaxFailures$1.evaluate(TestRuleIgnoreAfterMaxFailures.java:65)
    org.apache.lucene.util.TestRuleIgnoreTestSuites$1.evaluate(TestRuleIgnoreTestSuites.java:55)
    [...com.carrotsearch.randomizedtesting.*]
    java.lang.Thread.run(Unknown Source)

@Fitblip
Copy link
Contributor Author

Fitblip commented Jun 11, 2014

JsonpOptionEnabledTest test output:

[2014-06-10 22:54:25,483][INFO ][org.elasticsearch.test   ] Setup TestCluster [shared-doctor-PC-CHILD_VM=[0]-CLUSTER_SEED=[-5018658234085937559]-HASH=[2724E9BADD16B]] with seed [BA5A24ED1437EE69] using [3] data nodes and [0] client nodes
[2014-06-10 22:54:25,492][INFO ][org.elasticsearch.test   ] Test testThatJSONPisEnabled(org.elasticsearch.options.jsonp.JsonpOptionEnabledTest) started
[2014-06-10 22:54:25,500][INFO ][org.elasticsearch.test   ] Setup TestCluster [TEST-doctor-PC-CHILD_VM=[0]-CLUSTER_SEED=[-5583103424550574148]-HASH=[2724E9CC19E5F]] with seed [B284D504136567BC] using [1] data nodes and [0] client nodes
[2014-06-10 22:54:25,618][INFO ][org.elasticsearch.node   ] [node_0] version[2.0.0-SNAPSHOT], pid[11164], build[${build/NA]
[2014-06-10 22:54:25,618][INFO ][org.elasticsearch.node   ] [node_0] initializing ...
[2014-06-10 22:54:25,620][INFO ][org.elasticsearch.plugins] [node_0] loaded [], sites []
[2014-06-10 22:54:26,790][INFO ][org.elasticsearch.node   ] [node_0] initialized
[2014-06-10 22:54:26,790][INFO ][org.elasticsearch.node   ] [node_0] starting ...
[2014-06-10 22:54:26,896][INFO ][org.elasticsearch.test.transport] [node_0] bound_address {inet[/0:0:0:0:0:0:0:0:9300]}, publish_address {inet[/192.168.10.120:9300]}
[2014-06-10 22:54:29,988][INFO ][org.elasticsearch.cluster.service] [node_0] new_master [node_0][4fiTwUX6SA6IvLptNgNMRw][doctor-PC][inet[/192.168.10.120:9300]], reason: zen-disco-join (elected_as_master)
[2014-06-10 22:54:30,005][INFO ][org.elasticsearch.discovery] [node_0] TEST-doctor-PC-CHILD_VM=[0]-CLUSTER_SEED=[-5583103424550574148]-HASH=[2724E9CC19E5F]/4fiTwUX6SA6IvLptNgNMRw
[2014-06-10 22:54:30,014][INFO ][org.elasticsearch.gateway] [node_0] recovered [0] indices into cluster_state
[2014-06-10 22:54:30,041][INFO ][org.elasticsearch.http   ] [node_0] bound_address {inet[/0:0:0:0:0:0:0:0:9200]}, publish_address {inet[/192.168.10.120:9200]}
[2014-06-10 22:54:30,042][INFO ][org.elasticsearch.node   ] [node_0] started
[2014-06-10 22:54:30,042][INFO ][org.elasticsearch.test   ] Start Shared Node [node_0] not shared
[2014-06-10 22:54:30,048][INFO ][org.elasticsearch.plugins] [transport_client_node_0] loaded [], sites []
[2014-06-10 22:54:30,170][INFO ][org.elasticsearch.options.jsonp] [JsonpOptionEnabledTest#testThatJSONPisEnabled]: before test
[2014-06-10 22:54:30,207][INFO ][org.elasticsearch.options.jsonp] [JsonpOptionEnabledTest#testThatJSONPisEnabled]: cleaning up after test
[2014-06-10 22:54:30,238][INFO ][org.elasticsearch.node   ] [node_0] stopping ...
[2014-06-10 22:54:30,244][INFO ][org.elasticsearch.node   ] [node_0] stopped
[2014-06-10 22:54:30,244][INFO ][org.elasticsearch.node   ] [node_0] closing ...
[2014-06-10 22:54:30,246][INFO ][org.elasticsearch.node   ] [node_0] closed
[2014-06-10 22:54:30,246][INFO ][org.elasticsearch.options.jsonp] [JsonpOptionEnabledTest#testThatJSONPisEnabled]: cleaned up after test
[2014-06-10 22:54:30,247][INFO ][org.elasticsearch.test   ] Wipe data directory for all nodes locations: [C:\Users\doctor\Documents\GitHub\elasticsearch\data\TEST-doctor-PC-CHILD_VM=[0]-CLUSTER_SEED=[-5583103424550574148]-HASH=[2724E9CC19E5F]\nodes\0] success: true
[2014-06-10 22:54:30,264][INFO ][org.elasticsearch.test   ] Test testThatJSONPisEnabled(org.elasticsearch.options.jsonp.JsonpOptionEnabledTest) finished

@spinscale
Copy link
Contributor

couple of things here:

The test fails, because you do not free resources.. the builder actually has a close() method, that would be called, if you supplied it directly to the BytesRestResponse - luckily we catch those in the tests :-)

I suggest the following code to fix it and to have the right content type

                XContentBuilder builder = channel.newBuilder();
                builder.startObject().field("error","JSONP is disabled.").endObject();
                RestResponse response = new BytesRestResponse(FORBIDDEN, builder);
                response.addHeader("Content-Type", "application/javascript");
                channel.sendResponse(response);

If you do this, you can change the test to check for this as well

        assertThat(response.response(), containsString("JSONP is disabled"));
        assertThat(response.getHeader("Content-Type"), is("application/javascript"));

However, the test will fail, as there is a tiny bug in NettyHttpChannel.sendResponse(), that overrides the callback response. The method should check if the header is set already and only act if it isnt

            if (!resp.headers().contains(HttpHeaders.Names.CONTENT_TYPE)) {
                resp.headers().add(HttpHeaders.Names.CONTENT_TYPE, response.contentType());
            }
            if (!resp.headers().contains(HttpHeaders.Names.CONTENT_LENGTH)) {
                resp.headers().add(HttpHeaders.Names.CONTENT_LENGTH, String.valueOf(buffer.readableBytes()));
            }

I think thats it (with the exception of squashing) :-)

@Fitblip
Copy link
Contributor Author

Fitblip commented Jun 13, 2014

Wonderful! This is exactly what I was looking for. I'll get those commits
in tonight, and assuming I get the all-green from you squash everything
together, and we can get this closed!

Thanks for all your help in all this. If you ever find yourself on the west
coast, I definitely owe you some beers ;)

Ryan

On Fri, Jun 13, 2014 at 7:36 AM, Alexander Reelsen <notifications@github.com

wrote:

couple of things here:

The test fails, because you do not free resources.. the builder actually
has a close() method, that would be called, if you supplied it directly
to the BytesRestResponse - luckily we catch those in the tests :-)

I suggest the following code to fix it and to have the right content type

            XContentBuilder builder = channel.newBuilder();
            builder.startObject().field("error","JSONP is disabled.").endObject();
            RestResponse response = new BytesRestResponse(FORBIDDEN, builder);
            response.addHeader("Content-Type", "application/javascript");
            channel.sendResponse(response);

If you do this, you can change the test to check for this as well

    assertThat(response.response(), containsString("JSONP is disabled"));
    assertThat(response.getHeader("Content-Type"), is("application/javascript"));

However, the test will fail, as there is a tiny bug in
NettyHttpChannel.sendResponse(), that overrides the callback response.
The method should check if the header is set already and only act if it isnt

        if (!resp.headers().contains(HttpHeaders.Names.CONTENT_TYPE)) {
            resp.headers().add(HttpHeaders.Names.CONTENT_TYPE, response.contentType());
        }
        if (!resp.headers().contains(HttpHeaders.Names.CONTENT_LENGTH)) {
            resp.headers().add(HttpHeaders.Names.CONTENT_LENGTH, String.valueOf(buffer.readableBytes()));
        }

I think thats it (with the exception of squashing) :-)


Reply to this email directly or view it on GitHub
#6164 (comment)
.

@Fitblip
Copy link
Contributor Author

Fitblip commented Jun 14, 2014

@spinscale this should be good to go. All tests are green and everything works as expected. Squashing.

This adds the http.jsonp.enable option, which left enabled by default at the reqeust of the Elasticsearch folks. Allowing JSONP responses by default on all API endpoints poses a security risk, and probably shouldn't be used unless it's necessary.

This also squashes a couple bugs in NettyHttpChannel.java
 - 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
@spinscale spinscale closed this in d18fb8b Jun 19, 2014
spinscale pushed a commit that referenced this pull request Jun 19, 2014
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
@spinscale
Copy link
Contributor

Finally. Thanks a lot for all the time you invested here! Highly appreciated.

Will come back to your west coast offer on the long run :-)

@Fitblip
Copy link
Contributor Author

Fitblip commented Jun 19, 2014

Woo hoo! Thanks a lot for your help as well :)

@clintongormley clintongormley changed the title [Security] Make JSONP responses optional. Security: Make JSONP responses optional. Jul 16, 2014
@clintongormley clintongormley added the :Core/Infra/Settings Settings infrastructure and APIs label Jun 7, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants