Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14021889
D10291.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
12 KB
Referenced Files
None
Subscribers
None
D10291.diff
View Options
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -76,6 +76,7 @@
'AphrontProgressBarView' => 'view/widget/bars/AphrontProgressBarView.php',
'AphrontProxyResponse' => 'aphront/response/AphrontProxyResponse.php',
'AphrontRedirectResponse' => 'aphront/response/AphrontRedirectResponse.php',
+ 'AphrontRedirectResponseTestCase' => 'aphront/response/__tests__/AphrontRedirectResponseTestCase.php',
'AphrontReloadResponse' => 'aphront/response/AphrontReloadResponse.php',
'AphrontRequest' => 'aphront/AphrontRequest.php',
'AphrontRequestFailureView' => 'view/page/AphrontRequestFailureView.php',
@@ -2836,6 +2837,7 @@
'AphrontProgressBarView' => 'AphrontBarView',
'AphrontProxyResponse' => 'AphrontResponse',
'AphrontRedirectResponse' => 'AphrontResponse',
+ 'AphrontRedirectResponseTestCase' => 'PhabricatorTestCase',
'AphrontReloadResponse' => 'AphrontRedirectResponse',
'AphrontRequestFailureView' => 'AphrontView',
'AphrontRequestTestCase' => 'PhabricatorTestCase',
diff --git a/src/aphront/response/AphrontRedirectResponse.php b/src/aphront/response/AphrontRedirectResponse.php
--- a/src/aphront/response/AphrontRedirectResponse.php
+++ b/src/aphront/response/AphrontRedirectResponse.php
@@ -7,6 +7,12 @@
private $uri;
private $stackWhenCreated;
+ private $isExternal;
+
+ public function setIsExternal($external) {
+ $this->isExternal = $external;
+ return $this;
+ }
public function __construct() {
if ($this->shouldStopForDebugging()) {
@@ -21,7 +27,10 @@
}
public function getURI() {
- return (string)$this->uri;
+ // NOTE: When we convert a RedirectResponse into an AjaxResponse, we pull
+ // the URI through this method. Make sure it passes checks before we
+ // hand it over to callers.
+ return self::getURIForRedirect($this->uri, $this->isExternal);
}
public function shouldStopForDebugging() {
@@ -31,7 +40,8 @@
public function getHeaders() {
$headers = array();
if (!$this->shouldStopForDebugging()) {
- $headers[] = array('Location', $this->uri);
+ $uri = self::getURIForRedirect($this->uri, $this->isExternal);
+ $headers[] = array('Location', $uri);
}
$headers = array_merge(parent::getHeaders(), $headers);
return $headers;
@@ -85,4 +95,72 @@
return '';
}
+
+ /**
+ * Format a URI for use in a "Location:" header.
+ *
+ * Verifies that a URI redirects to the expected type of resource (local or
+ * remote) and formats it for use in a "Location:" header.
+ *
+ * The HTTP spec says "Location:" headers must use absolute URIs. Although
+ * browsers work with relative URIs, we return absolute URIs to avoid
+ * ambiguity. For example, Chrome interprets "Location: /\evil.com" to mean
+ * "perform a protocol-relative redirect to evil.com".
+ *
+ * @param string URI to redirect to.
+ * @param bool True if this URI identifies a remote resource.
+ * @return string URI for use in a "Location:" header.
+ */
+ public static function getURIForRedirect($uri, $is_external) {
+ $uri_object = new PhutilURI($uri);
+ if ($is_external) {
+ // If this is a remote resource it must have a domain set. This
+ // would also be caught below, but testing for it explicitly first allows
+ // us to raise a better error message.
+ if (!strlen($uri_object->getDomain())) {
+ throw new Exception(
+ pht(
+ 'Refusing to redirect to external URI "%s". This URI '.
+ 'is not fully qualified, and is missing a domain name. To '.
+ 'redirect to a local resource, remove the external flag.',
+ (string)$uri));
+ }
+
+ // Check that it's a valid remote resource.
+ if (!PhabricatorEnv::isValidRemoteWebResource($uri)) {
+ throw new Exception(
+ pht(
+ 'Refusing to redirect to external URI "%s". This URI '.
+ 'is not a valid remote web resource.',
+ (string)$uri));
+ }
+ } else {
+ // If this is a local resource, it must not have a domain set. This allows
+ // us to raise a better error message than the check below can.
+ if (strlen($uri_object->getDomain())) {
+ throw new Exception(
+ pht(
+ 'Refusing to redirect to local resource "%s". The URI has a '.
+ 'domain, but the redirect is not marked external. Mark '.
+ 'redirects as external to allow redirection off the local '.
+ 'domain.',
+ (string)$uri));
+ }
+
+ // If this is a local resource, it must be a valid local resource.
+ if (!PhabricatorEnv::isValidLocalWebResource($uri)) {
+ throw new Exception(
+ pht(
+ 'Refusing to redirect to local resource "%s". This URI is not '.
+ 'formatted in a recognizable way.',
+ (string)$uri));
+ }
+
+ // Fully qualify the result URI.
+ $uri = PhabricatorEnv::getURI((string)$uri);
+ }
+
+ return (string)$uri;
+ }
+
}
diff --git a/src/aphront/response/__tests__/AphrontRedirectResponseTestCase.php b/src/aphront/response/__tests__/AphrontRedirectResponseTestCase.php
new file mode 100644
--- /dev/null
+++ b/src/aphront/response/__tests__/AphrontRedirectResponseTestCase.php
@@ -0,0 +1,63 @@
+<?php
+
+final class AphrontRedirectResponseTestCase extends PhabricatorTestCase {
+
+ public function testLocalRedirectURIs() {
+ // Format a bunch of URIs for local and remote redirection, making sure
+ // we get the expected results.
+
+ $uris = array(
+ '/a' => array(
+ 'http://phabricator.example.com/a',
+ false,
+ ),
+ 'a' => array(
+ false,
+ false,
+ ),
+ '/\\evil.com' => array(
+ false,
+ false,
+ ),
+ 'http://www.evil.com/' => array(
+ false,
+ 'http://www.evil.com/',
+ ),
+ '//evil.com' => array(
+ false,
+ false,
+ ),
+ '//' => array(
+ false,
+ false,
+ ),
+ '' => array(
+ false,
+ false,
+ ),
+ );
+
+ foreach ($uris as $input => $cases) {
+ foreach (array(false, true) as $idx => $is_external) {
+ $expect = $cases[$idx];
+
+ $caught = null;
+ try {
+ $result = AphrontRedirectResponse::getURIForRedirect(
+ $input,
+ $is_external);
+ } catch (Exception $ex) {
+ $caught = $ex;
+ }
+
+ if ($expect === false) {
+ $this->assertTrue(($caught instanceof Exception), $input);
+ } else {
+ $this->assertEqual(null, $caught, $input);
+ $this->assertEqual($expect, $result, $input);
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/applications/auth/provider/PhabricatorOAuth1AuthProvider.php b/src/applications/auth/provider/PhabricatorOAuth1AuthProvider.php
--- a/src/applications/auth/provider/PhabricatorOAuth1AuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorOAuth1AuthProvider.php
@@ -62,7 +62,9 @@
$client_code,
$adapter->getTokenSecret());
- $response = id(new AphrontRedirectResponse())->setURI($uri);
+ $response = id(new AphrontRedirectResponse())
+ ->setIsExternal(true)
+ ->setURI($uri);
return array($account, $response);
}
diff --git a/src/applications/files/controller/PhabricatorFileDataController.php b/src/applications/files/controller/PhabricatorFileDataController.php
--- a/src/applications/files/controller/PhabricatorFileDataController.php
+++ b/src/applications/files/controller/PhabricatorFileDataController.php
@@ -74,6 +74,7 @@
// if the user can see the file, generate a token;
// redirect to the alt domain with the token;
return id(new AphrontRedirectResponse())
+ ->setIsExternal(true)
->setURI($file->getCDNURIWithToken());
} else {
@@ -128,7 +129,9 @@
// file cannot be served via cdn, and no token given
// redirect to the main domain to aquire a token
+ // This is marked as an "external" URI because it is fully qualified.
return id(new AphrontRedirectResponse())
+ ->setIsExternal(true)
->setURI($acquire_token_uri);
}
}
@@ -171,7 +174,10 @@
// authenticate users on the file domain. This should blunt any
// attacks based on iframes, script tags, applet tags, etc., at least.
// Send the user to the "info" page if they're using some other method.
+
+ // This is marked as "external" because it is fully qualified.
return id(new AphrontRedirectResponse())
+ ->setIsExternal(true)
->setURI(PhabricatorEnv::getProductionURI($file->getBestURI()));
}
$response->setMimeType($file->getMimeType());
diff --git a/src/applications/phortune/provider/PhortunePaypalPaymentProvider.php b/src/applications/phortune/provider/PhortunePaypalPaymentProvider.php
--- a/src/applications/phortune/provider/PhortunePaypalPaymentProvider.php
+++ b/src/applications/phortune/provider/PhortunePaypalPaymentProvider.php
@@ -116,7 +116,9 @@
'token' => $result['TOKEN'],
));
- return id(new AphrontRedirectResponse())->setURI($uri);
+ return id(new AphrontRedirectResponse())
+ ->setIsExternal(true)
+ ->setURI($uri);
case 'charge':
var_dump($_REQUEST);
break;
diff --git a/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php b/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php
--- a/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php
+++ b/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php
@@ -149,7 +149,9 @@
// user might not end up back here. Really this needs a bunch of junk.
$uri = new PhutilURI($result->checkout_uri);
- return id(new AphrontRedirectResponse())->setURI($uri);
+ return id(new AphrontRedirectResponse())
+ ->setIsExternal(true)
+ ->setURI($uri);
case 'charge':
$checkout_id = $request->getInt('checkout_id');
$params = array(
@@ -195,7 +197,9 @@
unset($unguarded);
- return id(new AphrontRedirectResponse())->setURI($cart_uri);
+ return id(new AphrontRedirectResponse())
+ ->setIsExternal(true)
+ ->setURI($cart_uri);
case 'cancel':
var_dump($_REQUEST);
break;
diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php
--- a/src/infrastructure/env/PhabricatorEnv.php
+++ b/src/infrastructure/env/PhabricatorEnv.php
@@ -462,6 +462,21 @@
return false;
}
+ // Chrome (at a minimum) interprets backslashes in Location headers and the
+ // URL bar as forward slashes. This is probably intended to reduce user
+ // error caused by confusion over which key is "forward slash" vs "back
+ // slash".
+ //
+ // However, it means a URI like "/\evil.com" is interpreted like
+ // "//evil.com", which is a protocol relative remote URI.
+ //
+ // Since we currently never generate URIs with backslashes in them, reject
+ // these unconditionally rather than trying to figure out how browsers will
+ // interpret them.
+ if (preg_match('/\\\\/', $uri)) {
+ return false;
+ }
+
// Valid URIs must begin with '/', followed by the end of the string or some
// other non-'/' character. This rejects protocol-relative URIs like
// "//evil.com/evil_stuff/".
diff --git a/src/infrastructure/env/__tests__/PhabricatorEnvTestCase.php b/src/infrastructure/env/__tests__/PhabricatorEnvTestCase.php
--- a/src/infrastructure/env/__tests__/PhabricatorEnvTestCase.php
+++ b/src/infrastructure/env/__tests__/PhabricatorEnvTestCase.php
@@ -13,6 +13,7 @@
'javascript:lol' => false,
'' => false,
null => false,
+ '/\\evil.com' => false,
);
foreach ($map as $uri => $expect) {
diff --git a/src/infrastructure/testing/PhabricatorTestCase.php b/src/infrastructure/testing/PhabricatorTestCase.php
--- a/src/infrastructure/testing/PhabricatorTestCase.php
+++ b/src/infrastructure/testing/PhabricatorTestCase.php
@@ -118,6 +118,10 @@
// TODO: Remove this when we remove "releeph.installed".
$this->env->overrideEnvConfig('releeph.installed', true);
+
+ $this->env->overrideEnvConfig(
+ 'phabricator.base-uri',
+ 'http://phabricator.example.com');
}
protected function didRunTests() {
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, Nov 7, 1:16 PM (1 w, 4 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6745625
Default Alt Text
D10291.diff (12 KB)
Attached To
Mode
D10291: Be more strict about "Location:" redirects
Attached
Detach File
Event Timeline
Log In to Comment