Problem/Motivation
Drupal 8 has a security issue very similar to the information disclosure vulnerability fixed years ago in SA-CORE-2009-005.
To reproduce this, you need to have Drupal 8 installed in the server root directory - for example, http://d8.dev
(not http://localhost/d8
).
- Apply the attached patch, which adds a very simple "Test form" block that contains a form (as might be provided by a contrib module).*
- Go to admin/structure/block and add "Test form" to the layout for Bartik (e.g. in the right sidebar). Under cache settings, set the maximum age to "no caching".**
-
Go to
http://d8.dev//example.org
and submit the form. Whatever data the site was trying to collect gets sent off to example.org.
Drupal 7 and earlier are not vulnerable to this because of the code added to request_uri() via the above security advisory:
// Prevent multiple slashes to avoid cross site requests via the Form API.
$uri = '/' . ltrim($uri, '/');
This code was removed in Drupal 8 via #2426489: Remove request_uri().
Notes:
* Standard core forms like the user login block and search block don't appear to be vulnerable, only because they happen to define a custom $form['#action'].
** Changing the cache settings isn't strictly necessary to reproduce this, but makes it easier. If you don't, you have to make sure http://d8.dev//example.org
is the very first page containing the form that you visit after clearing caches. This is due to a separate bug: #2504139: Blocks containing a form include the form action in the cache, so they always submit to the first URL the form was viewed at
Proposed resolution
The easiest fix would just be to add similar code as above to the form API when it's building the default $form['#action']. But I assume the security advisory added it to request_uri() instead to protect against other potentially vulnerable code that might need to access the request URI...
Comment | File | Size | Author |
---|---|---|---|
#66 | 2504141.66.patch | 8.38 KB | alexpott |
#66 | 62-66-interdiff.txt | 1.59 KB | alexpott |
#62 | 2504141.62.patch | 8.31 KB | alexpott |
#62 | 61-62-interdiff.txt | 1.04 KB | alexpott |
#61 | 2504141.61.patch | 8.19 KB | alexpott |
Comments
Comment #1
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedHere's the patch for manual testing purposes. Could become the basis for an automated test also.
Comment #2
xjmComment #3
dawehnerI was wondering whether this is something we should fix upstream
Comment #4
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedMaybe Symfony would accept it as a hardening patch? - I'm not sure. But I would guess their point of view is that they are just providing a low-level API that basically wraps $_SERVER['REQUEST_URI'] and that therefore aims to return the same thing it does, i.e. the request URI that the visitor sees in their browser.
It could be a separate method instead, I suppose. Not sure what the method would be named.
Comment #5
dawehnerI can see why they might not want that, but on the other hand they are easily running into the same issue.
Maybe a new method or parameter will be a good compromise.
What we should do for sure is to write test coverage for that bug. Sorry for not seeing it on the other issue.
Comment #6
larowlanTestbot runs in /checkout - this will make testing with a web-test difficult.
Having a crack at a unit test.
Comment #7
larowlanfailing unit test <3 FormInterface
Comment #8
dawehnerNote: contacted Javier Eguiluz (kinda the drupal symfony contact person) and he wants to open up an issue in the symfony issue describing the problem.
It seemed to be quite positive, from its feedback
Comment #11
larowlan@dawehner so should this move to the private issue tracker?
Comment #12
dawehnerI'm not sure, if I understand it correctly javier more thought about this as security hardening, let's see
Comment #13
alexpott@larowlan drupal 8 security issues that only affect drupal 8 are handled in public.
Comment #14
jibranI think he meant private issue tracker of symfony.
Comment #15
Fabianx CreditAttribution: Fabianx as a volunteer commentedIf upstream is up for it great, if not I would vote we override the Request object and provide our own class so that Request->getRequestUri() is always safe.
Great catch, David!
We should consider marking security hardenings with something like: @securityHardening or just "Security hardening: ..."
so that during code review removal of such code is a little more obvious - even though the code with "prevent CSRF" was pretty clear ...
Comment #17
larowlanyep I meant upstream
Drupal\system\Tests\Form\ExternalFormUrlTest 3 passes 2 fails
Comment #18
larowlanFails as expected
Comment #19
Crell CreditAttribution: Crell at Palantir.net commentedI talked to Matthew O'Phinney, and Zend Diactoros (the PSR-7 library) uses the ltrim() approach. Seems legit, although I agree, better upstream.
https://twitter.com/mwop/status/609046964747902976
Comment #22
larowlanUnpublishing until such time as symfony decide whether to fix it in public or privately
Comment #23
xjmThanks @larowlan, I was encouraging others to do the same and unpublish the issue earlier today.
@catch, @effulgentsia, @webchick, @alexpott and I agreed this issue is definitely critical for Drupal 8 core due to the exploitable security vulnerability.
If Symfony chooses to treat this issue as public security hardening then we can proceed with this as a public Drupal 8 issue at that time since it does not affect D7.
Comment #25
catchThis issue was unpublished to give Symfony time to decide 1. whether to fix the issue upstream 2. whether to do so privately via their security process if so.
The answer to both was 'no' after some discussion, so I'm re-publishing this.
Comment #28
tim.plunkett$form_state wasn't used, so that now-illegal setCached() call can go, and we can't call render() "outside of a render context".
This gives the appropriate test fails.
Comment #30
Wim Leers#28++ — I wanted to ask about that
setCached()
call, I suspected it to be illegal.Nit: s/urls/URLs/
It's kind of strange that a test implements
FormInterface
.Drupal doesn't render XML, but HTML. Let's use
loadHtml()
. Otherwise we may run into strange problems due to XML's more strict parsing.Is there a particular reason to not use
$this->xpath()
?Comment #31
tim.plunkett#30.2
grep for
extends KernelTestBase implements FormInterface
It's a beautiful thing for a self-contained test (able to call
\Drupal::formBuilder()->buildForm($this);
)Comment #32
dawehnerGiven that we already have our own Request factory already, see
\Drupal\Core\DrupalKernel::setupTrustedHosts
we could also manipulate the incoming request, which IMHO is not the worst idea, given that it just "normalizes" the request.
@larowlan is in his well earned holidays
Comment #33
tim.plunkettUsing the FAPI based solution from the IS, and addressing the rest of #30.
Comment #35
alexpottSeems a comment here would be good.
Comment #36
tim.plunkettBorrowing the comment from request_uri().
Do want to look into a more generic fix in this issue, or was this FAPI bit enough to fix the critical part, and have a major follow-up?
Comment #37
alexpottI think that we should look at a more generic issue because atm you probably could ddos a site with this. Since this is not normalised as it is in d7 filing up page cache is pretty simple
Comment #38
alexpottDiscussed at length with @ircmaxwell and @neclimdul in #drupal-wscci
The upshot is that we probably want to bear https://github.com/zendframework/zend-diactoros/commit/68fc7424a66735926... in mind.
We could just implement middleware or a request listener that redirects http://example.org///foo to http://example.org/foo and be done? That way we should be able to fix cache pollution, google ranking and this security issue all in one go?
Comment #39
alexpottSo at the moment d8 will produce 404's for
example.com//node/1
andexample.com//admin
which feels correct. So perhaps the idea to implement redirects is wrong. @ircmaxwell also pointed out that the use of relative urls in form actions is problematic for reasons like this. Why do we do that?Comment #40
dawehnerWell, I think this kinda a bad argument, given that you can pass along query attributes really easy and by that bypass page caching.
I can just guess that its historically driven. In case we would use absolute URLs, we would be more safe, this is for sure. The only tiny downsite I could see is that we need more cache entries, like for example one for
http://
and one forhttps://
URLs.So what about checking for "//" at the beginning of the request cycle and in case its there just throw a 404?
Comment #41
catchDrupal 7 page cache will let you do http://example.com/node/1/xyz so 8.x won't fill up any differently (i.e. https://www.drupal.org/node/1000/xyz).
Yes I don't think we can worry about this, or if we do we'd need to open another issue to fix the problem in many more places.
Also, are we sure search engines treat multiple slashes as separate URLs? I've never seen any indication that this is the case?
The middleware isn't necessarily a bad idea in itself, but I don't think it should block this issue, and unless there's a better indication that this is a real problem, I think it could live in global redirect module or similar.
Comment #42
dawehnerFair point, it would be additional security hardening.
The patch for itself looks reasonable for me. We have for example test coverage.
Comment #43
alexpottActually theoretically http://example.com//foo is a valid url (if a little weird). However adding
To node.routing does not work. I think the correct thing here is to add a security request listener that 404's for any path that starts with // and be done. Since our application just does not support url's with multiple leading slashes. If we did then this change would break forms that submitted back to them.
Comment #44
Wim Leers#43 sounds very sensible.
Comment #45
alexpottActually doing a 404 is not possible because the open redirect vulnerability exists on blocks on the 404 page which submit to the page they are on.
In D7 URL's like https://www.drupal.org///////////////////////////////planet work. In D8 this would result in a 404. I think the best solution is to redirect URI's which have leading slashes to a single slash.
This is a completely different approach so no interdiff.
I wonder if the test added works on testbot given it is has a base path.
Comment #46
Wim LeersShould this specify the exact type of event? Not sure.
Can be
@inheritdoc
.Let's use the short notation.
I think a comment like this would be helpful here:
P.S.: I'm pretty sure this is the most strange looking assertion in D8 :)
I don't think this comment is right?
Comment #48
alexpottOops #45 was a multi-patch. Fixed that and addressed everything from @Wim Leers review. Thanks @Wim Leers.
Comment #49
dawehnerAny reason we don't use a middleware approach?
Comment #51
dawehnerI'm totally fine with not using a middleware approach.
Comment #52
alexpottSo yep testbot exposed a weakness when dealing with drupal8 installed in a subdirectory. Also we need to deal with query strings.
Comment #53
neclimdulYeah like Wim said in #45, someone could put a login block or some other unsafe theme code on a 40X page. Redirect is the way to go.
re: #49 I suggested a listener in IRC mostly because as a general rule I have concerns about making the middleware stack too deep. IMHO if we can implement something as a listener we should as I feel like it gives us more flexibility and maintainability.
Comment #54
dawehnerWe should certainly add some integration test coverage, that includes a form
Comment #55
Crell CreditAttribution: Crell at Palantir.net commentedStarting with *multiple* leading slashes.
I'm otherwise on board with this approach, though I agree with dawehner that we should add a test with a malicious form just to be sure we're catching the security concern.
Comment #56
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedIn followup comments on the private security thread, the Symfony representative clarified that they actually only decided on #2 (not to treat it as a Symfony security issue). But if we want to propose that Symfony treat it as either a bugfix (by changing
Request::getRequestUri()
) or as a new feature (by adding something likeRequest::getValidRequestUri()
) that is still on the table for discussion.So if that's still something people want to try for, we can feel free to file a public Symfony issue about it.
Comment #57
dawehnerI think its worth to try that, see https://github.com/symfony/symfony/issues/15292
Comment #58
alexpottI still feel that the redirect path is worth pursuing because this makes D8 behaviour similarly to D7 in that the same page will end up being displayed.
I'm not sure how to add an explicit test for the form given then as the patch and test show it is not possible to get to a page with multiple leading slashes.
Comment #59
catchWe could do a negative test - make sure it's not a 200, and assertNoRaw() for the form action with the double slash? I usually don't like those but I think that works for covering the security issue itself.
Comment #60
dawehnerI don't disagree at all, but yeah I think the security issue is in the interaction between the form component and the $request, so the fix should be done there.
Doing stuff on top is certainly not a bad idea.
We could add a kernel test, change the container and deregister the listener.
Comment #61
alexpottHere's a test. I think we should use an absolute URL if the request URI starts with multiple slashes. So that it we every do support multiple leading slashes resolving to different routes (why I have no idea but who knows) the code here will not let us down.
The test added is jsut an adaptation of @tim.plunkett's test in #36
Comment #62
alexpottAddressing #55 point about the comment.
Comment #63
dawehnerWe have a proper test coverage now!
Comment #64
Crell CreditAttribution: Crell at Palantir.net commentedNitpick: It's not an official coding standard but by convention I think most of our subscriber classes have a *Subscriber suffix, and the service name has "subscriber" in it.
Actually, trolling through core.services.yml just now it seems that's only like 2/3 of the time. *sigh* Not a blocker here, just a note that we're being inconsistent.
Other than that, I agree this is ready to commit as soon as the bot is happy with it. Thanks, Alex!
Comment #65
Fabianx CreditAttribution: Fabianx as a volunteer commentedRTBC + 1, agree with #64 that the rename would be nice.
Comment #66
alexpottFine by me to rename it.
Comment #68
catchLooks right to me as well. I wasn't sure about the redirect, but given 7.x service pages like /////node1 it seems reasonable, and sites can override if they really don't like it.
Committed/pushed to 8.0.x, thanks!