Problem/Motivation
Going to a URL like http://example.com/foo?destination=http://example.com/bar
does not result in going to http://example.com/bar
This is purposely prevented by the code in drupal_goto()
for security:
// We do not allow absolute URLs to be passed via $_GET, as this can be an attack vector.
if (isset($_GET['destination']) && !url_is_external($_GET['destination'])) {
Proposed resolution
I'm proposing that we allow absolute URLs in $_GET['destination']
if and only if the domain is equal to our website's domain.
Remaining tasks
Commit #68 to 7.x.
User interface changes
None.
API changes
Going to a URL like http://example.com/foo?destination=http://example.com/bar
would result in going to http://example.com/bar
Original report by fizk
Going to a URL like http://example.com/foo?destination=http://example.com/bar
does not result in going to http://example.com/bar
This is purposely prevented by the code in drupal_goto()
for security:
// We do not allow absolute URLs to be passed via $_GET, as this can be an attack vector.
if (isset($_GET['destination']) && !url_is_external($_GET['destination'])) {
I'm proposing that we allow absolute URLs in $_GET['destination']
if and only if the domain is equal to our website's domain.
Comment | File | Size | Author |
---|---|---|---|
#97 | interdiff.txt | 665 bytes | David_Rothstein |
#97 | 736172-drupal-goto-same-domain-97.patch | 5.92 KB | David_Rothstein |
#68 | 736172-drupal-goto-same-domain-3-d7.patch | 5.73 KB | fizk |
#68 | 736172-drupal-goto-same-domain-3-d7-tests-only.patch | 3.7 KB | fizk |
#55 | 736172-drupal-goto-same-domain-8-interdiff.patch | 558 bytes | fizk |
Comments
Comment #1
fizk CreditAttribution: fizk commentedComment #2
fizk CreditAttribution: fizk commentedThis issue could also be titled, "?destination" query can't redirect forms to a secure path
Comment #3
harish.salian05 CreditAttribution: harish.salian05 commentedTry this it may work for u all,
I have used some conditions to get my desired output the redirect string also may contain ? in the string for e.g. index.php?test=test
function drupal_goto($path = '', $query = NULL, $fragment = NULL, $http_response_code = 302) {
$destination = FALSE;
$append='';
$url_append='';
$inc='';
if (isset($_REQUEST['destination'])) {
$destination = $_REQUEST['destination'];
// THIS IS THE CONDITION THAT I HAVE APPENDED
// STARTS HERE
if(isset($_GET['redirect']))
{
$destination = $_REQUEST['redirect'];
if(isset($_REQUEST['redr']) && $_REQUEST['redr']==1)
{
// I HAVE PASSED THE ENCODED STRING
$destination = base64_decode($_REQUEST['dest']); //DECODED HERE
if(strpos($destination,'?')===TRUE)
{
$inc=1;
$exp=explode('?',$destination);
$url_append=$exp[1];
}
}
$append="|| !preg_match('![/&#]!', substr($destination, 0, $colonpos))";
}
// ENDS HERE
}
else if (isset($_REQUEST['edit']['destination'])) {
$destination = $_REQUEST['edit']['destination'];
}
if ($destination) {
// Do not redirect to an absolute URL originating from user input.
$colonpos = strpos($destination, ':');
// The $append is appended the following condition
$absolute = ($colonpos !== FALSE && (!preg_match('![/?#]!', substr($destination, 0, $colonpos)) .$append));
if (!$absolute) {
extract(parse_url(urldecode($destination)));
}
}
$url = url($path, array('query' => $query, 'fragment' => $fragment, 'absolute' => TRUE));
// Remove newlines from the URL to avoid header injection attacks.
$url = str_replace(array("\n", "\r"), '', $url);
// Allow modules to react to the end of the page request before redirecting.
// We do not want this while running update.php.
if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') {
module_invoke_all('exit', $url);
}
// Even though session_write_close() is registered as a shutdown function, we
// need all session data written to the database before redirecting.
session_write_close();
global $base_url;
// Last and finally redirect
if($inc==1)
{
header('Location: '. $url.$url_append, TRUE, $http_response_code);
}
else
{
header('Location: '. $url, TRUE, $http_response_code);
}
// The "Location" header sends a redirect status code to the HTTP daemon. In
// some cases this can be wrong, so we make sure none of the code below the
// drupal_goto() call gets executed upon redirection.
exit();
}
Thanks.
Comment #4
valthebaldSince the last change, drupal_goto() has significantly changed.
I have used the following code to test redirect to the same domain:
to see if drupal_goto accepts absolute URLs to the current domain.
Same code worked correctly (redirect to help) for 8.x, 7.x and 6.x
Comment #5
fizk CreditAttribution: fizk commentedThe
drupal_goto()
function has not changed at all in 7.x and 6.x.Comment #6
valthebaldThen can you please describe what's wrong with it? The code in my previous comment works as expected
Comment #7
fizk CreditAttribution: fizk commentedGoing to a URL like
http://example.com/foo?destination=http://example.com/bar
does not result in going tohttp://example.com/bar
This is purposely prevented by the code in
drupal_goto()
for security:I'm proposing that we allow absolute URLs in
$_GET['destination']
if and only if the domain is equal to our website's domain.Comment #8
valthebaldComment #9
gregglesI think this proposal is fine. Open redirects is about getting redirected to somewhere else on the internet. If the site is the exact same domain I think it's fine. We probably shouldn't allow redirects to subdomains/parent domains.
I don't know that this is a bug or a feature, but my sense is that it should be fixed in 8 first, then 7, then 6 as is the normal flow.
Comment #10
fizk CreditAttribution: fizk commentedMarking 8.x-dev as per #8 and #9.
Comment #11
fizk CreditAttribution: fizk commentedPatch for 8.x-dev.
Comment #12
fizk CreditAttribution: fizk commentedComment #13
valthebaldI think patch should also handle situation with 2 sites with different base_paths, and one redirects to another.
This should be treated as external host (as with subdomains), right?
Comment #14
fizk CreditAttribution: fizk commentedvalthebald, if you mean
http://my-domain.com/node/1?destination=http://other-domain.com
, then this would be an unsafe open redirect as greggles mentioned and shouldn't be allowed.Comment #15
greggles@fizk - it would be great if you could add some tests to help confirm that the protection against redirection to external domains is working properly.
Comment #16
fizk CreditAttribution: fizk commented@greggles, no problem, was hoping to get your input on the patch before I started on the tests.
Comment #17
valthebald#14: I meant redirect from example.com/subfolder1/node/1 to example.com/subfolder2/node/2
Comment #18
fizk CreditAttribution: fizk commentedYes, with this patch,
http://example.com/subfolder1/node/1?destination=http://example.com/subfolder2/node/2
will work.I see what you mean now. Perhaps we should not only match the base_url, but also the base_path.
Comment #19
valthebald#18: Right, probably redirect to subfolder2 should be treated as external domain (just IMO, not 100% in that)
Comment #20
fizk CreditAttribution: fizk commentedI think you're right, I'll update the patch to match the base_url and base_path.
Comment #21
greggles@fizk - actually, I think #18 is a problem that would need protection against (which is what I think valthebald meant in #13). We need to prevent redirection from one Drupal site to another whether that site is on the same host or not.
Comment #22
gregglesx-post, but status change makes sense :)
Comment #23
PanchoYep. Must certainly be checked against base_path.
Furthermore: Why would this be a bug regarding core functionality rather than a new core feature?
And if it's indeed a feature: what are the use cases for adding this?
Finally: how do we cope with language subdomains that need to work transparently just as language prefixes?
Comment #24
fizk CreditAttribution: fizk commentedWe now check the domain and base_path.
Comment #25
fizk CreditAttribution: fizk commentedPancho,
IMHO it's a bug because we were trying to do something but didn't do it quite right the first time - i.e. there is no security benefit to preventing absolute URLs that actually point to your Drupal site.
I haven't worked with Drupal in non-English languages. Could you give me some example URLs?
Comment #26
fizk CreditAttribution: fizk commentedFix a mistake - helper function
_external_url_is_local()
didn't use its argument.Comment #27
fizk CreditAttribution: fizk commentedSlight tweak - only grab the host part when parsing base_url.
I'm going to add some simple tests next.
Comment #28
fizk CreditAttribution: fizk commentedPatch + simple tests.
Comment #29
valthebaldPatch looks fine for me, and covered by new test, which are ok for the test bot.
Marking RTBC
Comment #30
xjmThanks @fizk and @valthebald!
I found a few things to clean up, one of which is missing minimum documentation (see http://drupal.org/core-gates#documentation-block-requirements), so marking needs work.
These comments should be rewrapped so that the lines do not go over 80 characters. Reference: http://drupal.org/node/1354#general
Few things with this docblock:
@param
documenting the URL, and datatypes for the@param
and@return
. Reference: http://drupal.org/core-gates#documentation-block-requirementsReference: http://drupal.org/node/1354#functions
"paths" should not have an apostrophe.
When uploading a new patch, please include an interdiff, and also a test-only patch (which we expect to fail) to expose the test's coverage of the fix.
Comment #31
fizk CreditAttribution: fizk commentedThanks @xjm, here are the new patches.
@everyone, Please review and mark RTBC.
Comment #33
fizk CreditAttribution: fizk commentedSince no functionality has been changed, and the tests pass and fail as expected, I'm re-marking as RTBC.
Comment #34
valthebaldIt's usually not a good idea for patch author to mark issue RTBC... but I was going to do this anyway :)
Comment #35
Crell CreditAttribution: Crell commentedCatch pinged me on this issue. drupal_goto() is slated for removal and must be removed before we ship: #1668866: Replace drupal_goto() with RedirectResponse.
That issue isn't commitable yet, though, so I defer to catch on what order he wants to do things in. But this code will get rewritten before we finish.
Comment #36
fizk CreditAttribution: fizk commentedThanks Crell. #1668866: Replace drupal_goto() with RedirectResponse seems to have stalled for the last two months and still needs more work. This patch is ready to go today, so I don't see why committing this now would pose a problem.
Comment #37
catchOK then, this seems reasonable so I've gone ahead and committed/pushed to 8.x
Comment #38
fizk CreditAttribution: fizk commentedFirst core patch committed - w00t!! :P
Thanks everyone.
Comment #39
fizk CreditAttribution: fizk commentedBackporting to D7.
Comment #41
fizk CreditAttribution: fizk commented@valthebald, Can you please review and mark as RTBC :)
Comment #42
valthebaldConfirm that patch from #39 works the same for D7, as #31 for D8.
Also, test addition fails without patch, and passes with patch
Comment #43
valthebald@fizk, welcome to the club!
Comment #44
fizk CreditAttribution: fizk commented@catch, can you please commit the D7 backport in #39? Thanks.
Comment #45
David_Rothstein CreditAttribution: David_Rothstein commented@catch is the Drupal 8 maintainer, not Drupal 7 :)
I was going to commit this (as one of the last patches before the Drupal 7.17 release) but I think it could use a little more work. The URL matching seems a little iffy... for example, won't a URL like http://drupal.dev/node?destination=http://drupal.dev result in a PHP notice due to an undefined $url_parts['path']?
Also, it could use another review or two for code style issues. Some I noticed quickly:
This should be a complete sentence with a capital letter and a period.
(And by the way, why wasn't this comment in the Drupal 7 version of the patch?)
This function has the wrong indentation throughout (should be two spaces only).
Maybe just use http://example.com?
I'm not sure what "alt" refers to here, and normally we wouldn't use abbreviated words like this (although I see the surrounding code is full of them already, so whatever :)
In any case, do we really need this new menu item at all; couldn't the existing common-test/drupal_goto path be used in the tests instead?
The patch looks quite good otherwise, and I might have committed it with the code style issues, but especially given the PHP notices/functionality issue I mentioned above, I think it would be good to have one more round of work before getting this into Drupal 7...
Thanks!
Comment #46
fizk CreditAttribution: fizk commented@David_Rothstein, Thanks, updated patch includes changes 1-4 for 8.x-dev.
I had the same thought at first, but I strongly believe it makes the code much easier to understand using a new menu item.
You're right, good catch. I've added a
if (!isset($url_parts['path']))
test to the patch to check for this.Comment #47
fizk CreditAttribution: fizk commented@valthebald, RTBC? :)
Comment #48
gregglesSlight title tweak since it's not just about domain, but actually the whole site (including subdomains or subfolders).
Comment #49
fizk CreditAttribution: fizk commentedThe patch in #46 looks good to me. Waiting for someone else to RTBC.
Comment #50
valthebaldWhy does it refer to 8.x? I think patch was already committed to 8.x branch, so it should be backported now
Comment #51
fizk CreditAttribution: fizk commentedThe code committed to 8.x needs to be fixed. I can backport to 7.x after that.
Comment #52
fizk CreditAttribution: fizk commentedSeems this isn't moving forward due to the lack of RTBC status, so I'm changing the status. The patch hasn't changed too much anyway.
Comment #53
catchLooks fine to me except for one code style issue:
else should be on a new line.
Comment #54
fizk CreditAttribution: fizk commentedThanks catch! Here's the fixed patch against 8.x-dev as of today.
Comment #55
fizk CreditAttribution: fizk commentedAttaching interdiff.
Comment #57
Crell CreditAttribution: Crell commentedWell of course the interdiff failed, but the real patch worked. :-) (First patch in #54.)
Comment #58
catchOK. Committed/pushed to 8.x.
This looks backportable.
Comment #59
fizk CreditAttribution: fizk commentedUpdated the D7 patch using the corrections made to the D8 patch. No new features or tests.
Comment #61
fizk CreditAttribution: fizk commentedTest only patch failed as expected.
Comment #62
fizk CreditAttribution: fizk commented@David, I think this is good to go.
Comment #63
fizk CreditAttribution: fizk commented@David Do you see any remaining issues here?
Comment #64
David_Rothstein CreditAttribution: David_Rothstein commentedWell, you marked your own patch RTBC which is generally discouraged (especially for a non-trivial patch that has security implications) :)
I know there are other people interested in this issue so I'm surprised that in the last couple months no one else has reviewed it. In general, I really don't like to be the only person to review a patch before committing it. However, I know this one has gone through several rounds of reviews already so maybe it's OK (looks fine on a quick glance at any rate). I'll try to get a final review on this done and hopefully commit it soon.
Thanks!
Comment #65
David_Rothstein CreditAttribution: David_Rothstein commentedHm, is it a problem that with this patch, a URL like http://example.com/drupal/node?destination=http://example.com will successfully redirect you from the site at http://example.com/drupal to the site at http://example.com?
That's a little different than the examples I remember being discussed above (e.g. two different subfolders on the same domain) and seems less likely to be a problem than those, but I'm not 100% sure it's OK.
Also (comparitively minor): It looks like there will still be PHP notices if $url_parts['host'] is undefined, although I guess unlike $url_parts['path'] you'd need to construct a pathological example to run into that one.
Comment #66
fizk CreditAttribution: fizk commentedYes, that shouldn't be allowed, but I don't see how the patch in #59 would allow that?
Comment #67
David_Rothstein CreditAttribution: David_Rothstein commentedWhen I tried it out locally it seemed to (specifically with http://localhost/drupal/node?destination=http://localhost).
I think it's because of this part:
When the URL being redirected to in 'destination' doesn't have a path at all, it only cares about comparing the hosts.
Comment #68
fizk CreditAttribution: fizk commentedGah, thanks for catching that. I've changed the if statement to:
Comment #69
fizk CreditAttribution: fizk commentedD8 needs this fix too. I'll attach a D8 patch after the #68 tests are done.
Comment #71
fizk CreditAttribution: fizk commentedComment #73
fizk CreditAttribution: fizk commentedComment #74
fizk CreditAttribution: fizk commented@David_Rothstein, thanks for pointing out that previous fix. I think we should be good to go now.
Comment #75
PanchoRaising priority to major per #65 and #67.
Also switching back to D8, so the testbot can successfully apply the followup patch in #71. (Unfortunately the testbot isn't smart enough to figure out if a patch is D7 or D8 even if accordingly named.)
Comment #76
PanchoDocumented behaviour of drupal_goto was changed, so we need a change notification.
Comment #77
Pancho#71: 736172-drupal-goto-same-domain-3-d8.patch queued for re-testing.
Comment #78
PanchoChange notices are critical.
Comment #79
PanchoSorry, I was too fast. The followup is not yet committed.
Comment #80
dlu CreditAttribution: dlu commentedMoved to routing system per #2050763-16: Refine "base system" component (notes on refactoring of "base system" category here: https://docs.google.com/a/acquia.com/spreadsheet/ccc?key=0AusehVccVSq2dF...).
Comment #81
xjmAlright, so we'll write the change notice once the followup is in. Thanks!
Comment #81.0
xjmClarify the description.
Comment #82
xjm71: 736172-drupal-goto-same-domain-3-d8.patch queued for re-testing.
Comment #84
fizk CreditAttribution: fizk commented@xjm, Thanks for coming back to this. I don't think we should work on this patch until we have a commitment from someone with commit access that this will be committed as soon as the patch applies cleanly. This patch has gone through a lot of work and should have been committed a while ago.
Comment #85
xjm@fizk, the patch was never marked "Reviewed & tested by the community", so that is why it was never committed. Core committers look at every patch that is marked RTBC within 1-2 weeks, and much sooner than that typically for majors and criticals. What was missed was someone to review and RTBC the followup.
One thing that makes it harder to review the issue here is that there was already one commit and there is a different followup patch, so it is confusing. An update to the issue summary to clearly explain the situation would help as well. And a possible way to find reviewers is to reach out on #drupal-contribute in IRC once there's an updated patch and maybe an issue summary update.
Comment #86
xjmComment #87
greendemiurge CreditAttribution: greendemiurge commentedDrupal_goto() has been removed, so the current patch is no longer relevant.
Comment #88
fizk CreditAttribution: fizk commentedgreendemiurge, thanks for the heads up.
It looks like drupal_goto() was removed in this commit:
http://cgit.drupalcode.org/drupal/commit/?id=00cb147e146d24c87bc31462e9f...
Changing this issue to 7.x.
Comment #89
fizk CreditAttribution: fizk commentedMarking the 7.x. patch RTBC, since it's been here a while, tested, and still applies cleanly against 7.x-dev.
Comment #90
fizk CreditAttribution: fizk commentedComment #91
David_Rothstein CreditAttribution: David_Rothstein commentedI tried the latest D7 patch and it still seems a tiny bit fragile - for a site at http://example.com/drupal, http://example.com/drupal/node/1?destination=http://example.com/drupal/ works but http://example.com/drupal/node/1?destination=http://example.com/drupal doesn't?
I also noticed that this allows an attacker to force a redirect from HTTPS to HTTP. I am not sure if that's a problem or not...
It also looks like the patch might need a reroll - retesting now.
Comment #93
David_Rothstein CreditAttribution: David_Rothstein commentedI think this was raised to Major priority for the D8 followup, which is no longer relevant. Restoring the original priority.
Comment #95
David_Rothstein CreditAttribution: David_Rothstein commentedOK, needs work - at the very least because the patch doesn't apply, but also see above.
Sorry. I feel bad about how long this issue is taking to get in, but it seems like there aren't many people interested in reviewing this particular patch.
Comment #96
fizk CreditAttribution: fizk commentedThanks for coming back to this David, I appreciate your efforts. Let's make it our goal to focus on this patch for this week. We probably won't need much more time than that.
This shouldn't be an issue. If an attacker can give you a URL to follow like https://example.com?destination=http://example.com , then they can also simply give you http://example.com . HTTPS isn't enforced by not having HTTP links on the page - although that helps - the site or web server needs to actively redirect HTTP requests to the HTTPS equivalent.
Comment #97
David_Rothstein CreditAttribution: David_Rothstein commentedYeah, I think you're right - if a site requires HTTPS for security it really needs to be forcing it, so there's no issue there.
OK, here's a reroll (the conflict in the test file was trivial). I have also added some code to fix the trailing slash case I mentioned in #91.
Unfortunately I discovered another problem while doing that. There is still a way to redirect from
http://example.com/drupal
tohttp://example.com
. You can do it with a URL like this:http://example.com/drupal/node/1?destination=http://example.com/drupal/../
That is probably possible to fix, but worse, what about the other way around? By definition this patch allows you to redirect from
http://example.com
tohttp://example.com/drupal
because it does not know thathttp://example.com/drupal
is a separate site (rather than a page on the parent site)... So if the goal really is as stated in #21, "We need to prevent redirection from one Drupal site to another whether that site is on the same host or not" then unfortunately I'm not sure this is even possible?Guess that needs some discussion. I'm setting needs review now for the testbot.
Comment #98
fizk CreditAttribution: fizk commentedThanks for the new patch.
I'll add some code to parse the "/../" parent paths in
_external_url_is_local()
.You can redirect from http://example.com to http://example.com/drupal without this patch using http://example.com/user?destination=drupal , so we're not introducing a new security issue.
Comment #99
David_Rothstein CreditAttribution: David_Rothstein commentedThat is a very good point :) D'oh!