Commit 4e0be1f3 authored by rob's avatar rob Committed by Commit bot

In Chromium, requests can be redirected before they hit the network by (re)starting

the request with a URLRequestRedirectJob. This is used by HSTS, the extension
webRequest API and protocol handlers.

These redirects are trusted and must be followed. However when such redirects are
triggered  for a cross-origin resource, e.g. <img src=".." crossorigin="anonymous">,
Blink blocks the redirect because the Access-Control-Allow-{Origin,Credentials}
response headers are missing.
This CL adds these headers to fix the problem.

Adding these CORS headers to the redirect response is safe, because CORS is still
enforced at the redirect target. For example, if HSTS is active for google.com and
an evil page embeds <img src="http://google.com/" crossorigin="use-credentials">,
then the image is not displayed because google.com does not reply with
"Access-Control-Allow-Origin: null".

BUG=387198
TEST=ExtensionWebRequestApiTest.WebRequestBlocking, HTTPSRequestTest.HSTSCrossOriginAddHeaders

Review URL: https://codereview.chromium.org/348253002

Cr-Commit-Position: refs/heads/master@{#294494}
parent 646de7d8
<script>
var img = document.createElement('img');
img.crossOrigin = location.search.match(/crossOrigin=(anonymous|use-credentials)/)[1];
// After verifying that the image is loaded correctly, we
// must also check whether the renderer accepts the resource.
// The replies are delayed to allow tests to close the tabs in case
// the test failed at an early stage.
img.onerror = function() {
setTimeout(function() {
// Should not happen. Failure is signaled to allow the unit tests to continue
// with the next test, the exact value of the next string does not matter.
new Image().src = '/signal_that_image_failed_to_load';
}, 500);
};
img.onload = function() {
setTimeout(function() {
// Verified in test_blocking.js
new Image().src = '/signal_that_image_loaded_successfully';
}, 500);
};
img.src = decodeURIComponent(location.search.match(/src=([^&]+)/)[1]);
</script>
HTTP/1.1 200 OK
Content-type: image/gif
X-Comment: Origin is null because the redirect target is on a different origin/
X-Comment: See "CORS 7.1.7 Generic Cross-Origin Request Algorithms,"
X-Comment: http://www.w3.org/TR/cors/#redirect-steps
X-Comment: "If the request URL origin is not same origin with the original URL origin, set
X-Comment: source origin to a globally unique identifier (becomes "null" when transmitted)."
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true
......@@ -322,6 +322,16 @@ runTests([
navigateAndWait(getURL("complexLoad/a.html"));
},
// Tests redirect of <img crossorigin="anonymous" src="...">
function crossOriginAnonymousRedirect() {
testLoadCORSImage("anonymous");
},
// Tests redirect of <img crossorigin="use-credentials" src="...">
function crossOriginCredentialedRedirect() {
testLoadCORSImage("use-credentials");
},
// Loads a testserver page that echoes the User-Agent header that was
// sent to fetch it. We modify the outgoing User-Agent in
// onBeforeSendHeaders, so we should see that modified version.
......@@ -994,3 +1004,130 @@ runTests([
});
},
]);
// This helper verifies that extensions can successfully redirect resources even
// if cross-origin access control is in effect via the crossorigin attribute.
// Used by crossOriginAnonymousRedirect and crossOriginCredentialedRedirect.
function testLoadCORSImage(crossOriginAttributeValue) {
// (Non-existent) image URL, with random query string to bust the cache.
var requestedUrl = getServerURL("cors/intercepted_by_extension.gif?" +
Math.random(), "original.tld");
var frameUrl = getServerURL(
"extensions/api_test/webrequest/cors/load_image.html?" +
"crossOrigin=" + crossOriginAttributeValue +
"&src=" + encodeURIComponent(requestedUrl));
var redirectTarget = getServerURL(
"extensions/api_test/webrequest/cors/redirect_target.gif", "domain.tld");
expect(
[ // events
{ label: "onBeforeRequest-1",
event: "onBeforeRequest",
details: {
type: "image",
url: requestedUrl,
// Frame URL unavailable because requests are filtered by type=image.
frameUrl: "unknown frame URL",
},
retval: {redirectUrl: redirectTarget}
},
{ label: "onBeforeRedirect",
event: "onBeforeRedirect",
details: {
type: "image",
url: requestedUrl,
redirectUrl: redirectTarget,
statusLine: "HTTP/1.1 307 Internal Redirect",
statusCode: 307,
fromCache: false,
}
},
{ label: "onBeforeRequest-2",
event: "onBeforeRequest",
details: {
type: "image",
url: redirectTarget,
// Frame URL unavailable because requests are filtered by type=image.
frameUrl: "unknown frame URL",
},
},
{
label: "onBeforeSendHeaders",
event: "onBeforeSendHeaders",
details: {
type: "image",
url: redirectTarget,
}
},
{
label: "onSendHeaders",
event: "onSendHeaders",
details: {
type: "image",
url: redirectTarget,
}
},
{
label: "onHeadersReceived",
event: "onHeadersReceived",
details: {
type: "image",
url: redirectTarget,
statusLine: "HTTP/1.1 200 OK",
}
},
{ label: "onResponseStarted",
event: "onResponseStarted",
details: {
type: "image",
url: redirectTarget,
fromCache: false,
statusCode: 200,
ip: "127.0.0.1",
statusLine: "HTTP/1.1 200 OK",
}
},
{ label: "onCompleted",
event: "onCompleted",
details: {
type: "image",
url: redirectTarget,
fromCache: false,
statusCode: 200,
ip: "127.0.0.1",
statusLine: "HTTP/1.1 200 OK",
}
},
// After the image loads, the test will load the following URL
// to signal that the test succeeded.
{
label: "onBeforeRequest-3",
event: "onBeforeRequest",
details: {
type: "image",
url: getServerURL("signal_that_image_loaded_successfully"),
// Frame URL unavailable because requests are filtered by type=image.
frameUrl: "unknown frame URL",
},
retval: {cancel: true}
},
{ label: "onErrorOccurred",
event: "onErrorOccurred",
details: {
type: "image",
url: getServerURL("signal_that_image_loaded_successfully"),
fromCache: false,
error: "net::ERR_BLOCKED_BY_CLIENT",
}
},
],
[ // event order
["onBeforeRequest-1", "onBeforeRedirect", "onBeforeRequest-2",
"onBeforeSendHeaders", "onSendHeaders", "onHeadersReceived",
"onResponseStarted", "onCompleted",
"onBeforeRequest-3", "onErrorOccurred"],
],
{urls: ["<all_urls>"], types: ['image']}, // filter
["blocking"]);
navigateAndWait(frameUrl);
}
......@@ -89,6 +89,23 @@ void URLRequestRedirectJob::StartAsync() {
response_code_,
redirect_destination_.spec().c_str(),
redirect_reason_.c_str());
std::string http_origin;
const net::HttpRequestHeaders& request_headers =
request_->extra_request_headers();
if (request_headers.GetHeader("Origin", &http_origin)) {
// If this redirect is used in a cross-origin request, add CORS headers to
// make sure that the redirect gets through. Note that the destination URL
// is still subject to the usual CORS policy, i.e. the resource will only
// be available to web pages if the server serves the response with the
// required CORS response headers.
header_string += base::StringPrintf(
"\n"
"Access-Control-Allow-Origin: %s\n"
"Access-Control-Allow-Credentials: true",
http_origin.c_str());
}
fake_headers_ = new HttpResponseHeaders(
HttpUtil::AssembleRawHeaders(header_string.c_str(),
header_string.length()));
......
......@@ -17,9 +17,11 @@ class GURL;
namespace net {
// A URLRequestJob that will redirect the request to the specified
// URL. This is useful to restart a request at a different URL based
// on the result of another job.
// A URLRequestJob that will redirect the request to the specified URL. This is
// useful to restart a request at a different URL based on the result of another
// job. The redirect URL could be visible to scripts if the redirect points to
// a same-origin URL, or if the redirection target is served with CORS response
// headers.
class NET_EXPORT URLRequestRedirectJob : public URLRequestJob {
public:
// Valid status codes for the redirect job. Other 30x codes are theoretically
......
......@@ -6720,6 +6720,74 @@ TEST_F(HTTPSRequestTest, HSTSPreservesPosts) {
TestLoadTimingCacheHitNoNetwork(load_timing_info);
}
// Make sure that the CORS headers are added to cross-origin HSTS redirects.
TEST_F(HTTPSRequestTest, HSTSCrossOriginAddHeaders) {
static const char kOriginHeaderValue[] = "http://www.example.com";
SpawnedTestServer::SSLOptions ssl_options(
SpawnedTestServer::SSLOptions::CERT_OK);
SpawnedTestServer test_server(
SpawnedTestServer::TYPE_HTTPS,
ssl_options,
base::FilePath(FILE_PATH_LITERAL("net/data/ssl")));
ASSERT_TRUE(test_server.Start());
// Per spec, TransportSecurityState expects a domain name, rather than an IP
// address, so a MockHostResolver is needed to redirect example.net to the
// SpawnedTestServer. MockHostResolver maps all hosts to 127.0.0.1 by default.
MockHostResolver host_resolver;
TransportSecurityState transport_security_state;
base::Time expiry = base::Time::Now() + base::TimeDelta::FromDays(1);
bool include_subdomains = false;
transport_security_state.AddHSTS("example.net", expiry, include_subdomains);
TestNetworkDelegate network_delegate; // Must outlive URLRequest.
MockCertVerifier cert_verifier;
cert_verifier.set_default_result(OK);
TestURLRequestContext context(true);
context.set_host_resolver(&host_resolver);
context.set_transport_security_state(&transport_security_state);
context.set_network_delegate(&network_delegate);
context.set_cert_verifier(&cert_verifier);
context.Init();
GURL hsts_http_url(base::StringPrintf("http://example.net:%d/somehstssite",
test_server.host_port_pair().port()));
url::Replacements<char> replacements;
const char kNewScheme[] = "https";
replacements.SetScheme(kNewScheme, url::Component(0, strlen(kNewScheme)));
GURL hsts_https_url = hsts_http_url.ReplaceComponents(replacements);
TestDelegate d;
// Quit on redirect to allow response header inspection upon redirect.
d.set_quit_on_redirect(true);
scoped_ptr<URLRequest> req(context.CreateRequest(hsts_http_url,
DEFAULT_PRIORITY, &d, NULL));
// Set Origin header to simulate a cross-origin request.
HttpRequestHeaders request_headers;
request_headers.SetHeader("Origin", kOriginHeaderValue);
req->SetExtraRequestHeaders(request_headers);
req->Start();
base::RunLoop().Run();
EXPECT_EQ(1, d.received_redirect_count());
const HttpResponseHeaders* headers = req->response_headers();
std::string redirect_location;
EXPECT_TRUE(headers->EnumerateHeader(NULL, "Location", &redirect_location));
EXPECT_EQ(hsts_https_url.spec(), redirect_location);
std::string received_cors_header;
EXPECT_TRUE(headers->EnumerateHeader(NULL, "Access-Control-Allow-Origin",
&received_cors_header));
EXPECT_EQ(kOriginHeaderValue, received_cors_header);
}
namespace {
class SSLClientAuthTestDelegate : public TestDelegate {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment