Commit 73e22231 authored by Tsuyoshi Horo's avatar Tsuyoshi Horo Committed by Commit Bot

CORS check for prefetched subresource signed exchanges

Bug: 935267
Change-Id: I453bec48331cdbc203f96ccd3c99168f2f1734d4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1616937
Commit-Queue: Tsuyoshi Horo <horo@chromium.org>
Reviewed-by: default avatarKinuko Yasuda <kinuko@chromium.org>
Reviewed-by: default avatarTakashi Toyoshima <toyoshim@chromium.org>
Reviewed-by: default avatarKunihiko Sakamoto <ksakamoto@chromium.org>
Cr-Commit-Position: refs/heads/master@{#662114}
parent 6e06473c
......@@ -16,6 +16,7 @@
#include "net/http/http_util.h"
#include "net/url_request/redirect_util.h"
#include "services/network/public/cpp/constants.h"
#include "services/network/public/cpp/cors/cors.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/resource_response.h"
......@@ -116,6 +117,7 @@ class RedirectResponseURLLoader : public network::mojom::URLLoader {
class InnerResponseURLLoader : public network::mojom::URLLoader {
public:
InnerResponseURLLoader(
const network::ResourceRequest& request,
const network::ResourceResponseHead& inner_response,
std::unique_ptr<const storage::BlobDataHandle> blob_data_handle,
const network::URLLoaderCompletionStatus& completion_status,
......@@ -125,6 +127,25 @@ class InnerResponseURLLoader : public network::mojom::URLLoader {
completion_status_(completion_status),
client_(std::move(client)),
weak_factory_(this) {
DCHECK(inner_response.headers);
DCHECK(request.request_initiator);
if (network::cors::ShouldCheckCors(request.url, request.request_initiator,
request.fetch_request_mode)) {
const auto error_status = network::cors::CheckAccess(
request.url, inner_response.headers->response_code(),
GetHeaderString(
inner_response,
network::cors::header_names::kAccessControlAllowOrigin),
GetHeaderString(
inner_response,
network::cors::header_names::kAccessControlAllowCredentials),
request.fetch_credentials_mode, *request.request_initiator);
if (error_status) {
client_->OnComplete(network::URLLoaderCompletionStatus(*error_status));
return;
}
}
network::ResourceResponseHead response = inner_response;
UpdateRequestResponseStartTime(&response);
response.encoded_data_length = 0;
......@@ -141,6 +162,16 @@ class InnerResponseURLLoader : public network::mojom::URLLoader {
~InnerResponseURLLoader() override {}
private:
static base::Optional<std::string> GetHeaderString(
const network::ResourceResponseHead& response,
const std::string& header_name) {
DCHECK(response.headers);
std::string header_value;
if (!response.headers->GetNormalizedHeader(header_name, &header_value))
return base::nullopt;
return header_value;
}
// network::mojom::URLLoader overrides:
void FollowRedirect(const std::vector<std::string>& removed_headers,
const net::HttpRequestHeaders& modified_headers,
......@@ -238,10 +269,9 @@ class SubresourceSignedExchangeURLLoaderFactory
network::mojom::URLLoaderClientPtr client,
const net::MutableNetworkTrafficAnnotationTag&
traffic_annotation) override {
// TODO(crbug.com/935267): Implement CORS check.
DCHECK_EQ(request.url, entry_->inner_url());
mojo::MakeStrongBinding(std::make_unique<InnerResponseURLLoader>(
*entry_->inner_response(),
request, *entry_->inner_response(),
std::make_unique<const storage::BlobDataHandle>(
*entry_->blob_data_handle()),
*entry_->completion_status(), std::move(client),
......@@ -340,7 +370,7 @@ class PrefetchedNavigationLoaderInterceptor
network::mojom::URLLoaderClientPtr client) {
mojo::MakeStrongBinding(
std::make_unique<InnerResponseURLLoader>(
*exchange_->inner_response(),
resource_request, *exchange_->inner_response(),
std::make_unique<const storage::BlobDataHandle>(
*exchange_->blob_data_handle()),
*exchange_->completion_status(), std::move(client),
......
......@@ -5,8 +5,13 @@
#include <string>
#include <vector>
#include "base/run_loop.h"
#include "base/stl_util.h"
#include "base/strings/stringprintf.h"
#include "base/task_runner.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_timeouts.h"
#include "base/threading/thread_task_runner_handle.h"
#include "content/browser/blob_storage/chrome_blob_storage_context.h"
#include "content/browser/frame_host/render_frame_host_impl.h"
#include "content/browser/loader/prefetch_browsertest_base.h"
......@@ -985,6 +990,230 @@ IN_PROC_BROWSER_TEST_P(
EXPECT_EQ(1, script2_fetch_count);
}
IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest, CORS) {
std::unique_ptr<net::EmbeddedTestServer> data_server =
std::make_unique<net::EmbeddedTestServer>(
net::EmbeddedTestServer::TYPE_HTTPS);
const char* prefetch_path = "/prefetch.html";
const char* target_sxg_path = "/target.sxg";
const char* target_path = "/target.html";
RegisterRequestHandler(embedded_test_server());
RegisterRequestHandler(data_server.get());
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(data_server->Start());
std::string test_server_origin = embedded_test_server()->base_url().spec();
// Remove the last "/""
test_server_origin.pop_back();
struct {
// Set in the main SXG's inner response header.
// Example:
// Link: <http://***/**.data>;rel="preload";as="fetch";crossorigin
// ^^^^^^^^^^^
const char* const crossorigin_preload_header;
// Set in the data SXG's inner response header.
// Example:
// Access-Control-Allow-Origin: *
// ^
const char* const access_control_allow_origin_header;
// Set "Access-Control-Allow-Credentials: true" link header in the data
// SXG's inner response header.
const bool has_access_control_allow_credentials_true_header;
// The credentials attribute of Fetch API's request while fetching the data.
const char* const request_credentials;
// If the data is served from the SXG the result must be "sxg". If the data
// is served from the server without SXG, the result must be "server". If
// failed to fetch the data, the result must be "failed".
const char* const expected_result;
} kTestCases[] = {
// - If crossorigin is not set in the preload header, cross origin fetch
// goes to the server. It is because the mode of the preload request
// ("no-cors") and the mode of the fetch request ("cors") doesn't match.
{nullptr, nullptr, false, "omit", "server"},
{nullptr, nullptr, false, "include", "server"},
{nullptr, nullptr, false, "same-origin", "server"},
// - When "crossorigin" is set in the preload header, the mode of the
// preload request is "cors", and the credentials mode is "same-origin".
// - When the credentials mode of the fetch request doesn't match, the
// fetch request goes to the server.
{"crossorigin", nullptr, false, "omit", "server"},
{"crossorigin", nullptr, false, "include", "server"},
// - When the credentials mode of the fetch request match with the
// credentials mode of the preload request, the prefetched signed
// exchange is used.
// - When the inner response doesn't have ACAO header, fails to load.
{"crossorigin", nullptr, false, "same-origin", "failed"},
// - When the inner response has "ACAO: *" header, succeeds to load.
{"crossorigin", "*", false, "same-origin", "sxg"},
// - When the inner response has "ACAO: <origin>" header, succeeds to
// load.
{"crossorigin", test_server_origin.c_str(), false, "same-origin", "sxg"},
// - crossorigin="anonymous" must be treated as same as just having
// "crossorigin".
{"crossorigin=\"anonymous\"", nullptr, false, "omit", "server"},
{"crossorigin=\"anonymous\"", nullptr, false, "include", "server"},
{"crossorigin=\"anonymous\"", nullptr, false, "same-origin", "failed"},
{"crossorigin=\"anonymous\"", "*", false, "same-origin", "sxg"},
{"crossorigin=\"anonymous\"", test_server_origin.c_str(), false,
"same-origin", "sxg"},
// - When crossorigin="use-credentials" is set in the preload header, the
// mode of the preload request is "cors", and the credentials mode is
// "include".
// - When the credentials mode of the fetch request doesn't match, the
// fetch request goes to the server.
{"crossorigin=\"use-credentials\"", nullptr, false, "omit", "server"},
{"crossorigin=\"use-credentials\"", nullptr, false, "same-origin",
"server"},
// - When the credentials mode of the fetch request match with the
// credentials mode of the preload request, the prefetched signed
// exchange is used.
// - When the inner response doesn't have ACAO header, fails to load.
{"crossorigin=\"use-credentials\"", nullptr, false, "include", "failed"},
// - Even if the inner response has "ACAO: *" header, fails to load
// the "include" credentials mode request.
{"crossorigin=\"use-credentials\"", "*", false, "include", "failed"},
// - Even if the inner response has "ACAO: *" header and "ACAC: true"
// header, fails to load the "include" credentials mode request.
{"crossorigin=\"use-credentials\"", "*", true, "include", "failed"},
// - Even if the inner response has "ACAO: <origin>" header, fails to
// load the "include" credentials mode request.
{"crossorigin=\"use-credentials\"", test_server_origin.c_str(), false,
"include", "failed"},
// - If the inner response has "ACAO: <origin>" header, and
// "ACAC: true" header, succeeds to load.
{"crossorigin=\"use-credentials\"", test_server_origin.c_str(), true,
"include", "sxg"},
};
const GURL target_sxg_url = embedded_test_server()->GetURL(target_sxg_path);
const GURL target_url = embedded_test_server()->GetURL(target_path);
const net::SHA256HashValue target_header_integrity = {{0x01}};
std::string target_sxg_outer_link_header;
std::string target_sxg_inner_link_header("Link: ");
std::string requests_list_string;
std::vector<MockSignedExchangeHandlerParams> mock_params;
for (size_t i = 0; i < base::size(kTestCases); ++i) {
if (i) {
target_sxg_outer_link_header += ",";
target_sxg_inner_link_header += ",";
requests_list_string += ",";
}
const std::string data_sxg_path = base::StringPrintf("/%zu_data.sxg", i);
const std::string data_path = base::StringPrintf("/%zu.data", i);
const GURL data_sxg_url = embedded_test_server()->GetURL(data_sxg_path);
const GURL data_url = data_server->GetURL(data_path);
requests_list_string += base::StringPrintf(
"new Request('%s', {credentials: '%s'})", data_url.spec().c_str(),
kTestCases[i].request_credentials);
const net::SHA256HashValue data_header_integrity = {{0x02 + i}};
const std::string data_header_integrity_string =
GetHeaderIntegrityString(data_header_integrity);
target_sxg_outer_link_header += base::StringPrintf(
"<%s>;rel=\"alternate\";type=\"application/signed-exchange;v=b3\";"
"anchor=\"%s\"",
data_sxg_url.spec().c_str(), data_url.spec().c_str());
target_sxg_inner_link_header += base::StringPrintf(
"<%s>;rel=\"allowed-alt-sxg\";header-integrity=\"%s\","
"<%s>;rel=\"preload\";as=\"fetch\"",
data_url.spec().c_str(), data_header_integrity_string.c_str(),
data_url.spec().c_str());
if (kTestCases[i].crossorigin_preload_header) {
target_sxg_inner_link_header +=
base::StringPrintf(";%s", kTestCases[i].crossorigin_preload_header);
}
RegisterResponse(data_sxg_path,
ResponseEntry("sxg", "application/signed-exchange;v=b3",
{{"x-content-type-options", "nosniff"}}));
RegisterResponse(
data_path,
ResponseEntry(
"server", "text/plain",
{{"Access-Control-Allow-Origin", test_server_origin.c_str()},
{"Access-Control-Allow-Credentials", "true"}}));
std::vector<std::string> data_sxg_inner_headers;
if (kTestCases[i].access_control_allow_origin_header) {
data_sxg_inner_headers.emplace_back(
base::StringPrintf("Access-Control-Allow-Origin: %s",
kTestCases[i].access_control_allow_origin_header));
}
if (kTestCases[i].has_access_control_allow_credentials_true_header) {
data_sxg_inner_headers.emplace_back(
"Access-Control-Allow-Credentials: true");
}
mock_params.emplace_back(
data_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK, data_url,
"text/plain", std::move(data_sxg_inner_headers), data_header_integrity);
}
std::vector<std::string> target_sxg_inner_headers = {
std::move(target_sxg_inner_link_header)};
mock_params.emplace_back(target_sxg_url, SignedExchangeLoadResult::kSuccess,
net::OK, target_url, "text/html",
std::move(target_sxg_inner_headers),
target_header_integrity);
MockSignedExchangeHandlerFactory factory(std::move(mock_params));
RegisterResponse(
prefetch_path,
ResponseEntry(base::StringPrintf(
"<body><link rel='prefetch' href='%s'></body>", target_sxg_path)));
RegisterResponse(
target_sxg_path,
// We mock the SignedExchangeHandler, so just return a HTML
// content as "application/signed-exchange;v=b3".
ResponseEntry(base::StringPrintf(R"(
<head><title>Prefetch Target (SXG)</title><script>
let results = [];
(async function(requests) {
for (let i = 0; i < requests.length; ++i) {
try {
const res = await fetch(requests[i]);
results.push(await res.text());
} catch (err) {
results.push('failed');
}
}
document.title = 'done';
})([%s]);
</script></head>)",
requests_list_string.c_str()),
"application/signed-exchange;v=b3",
{{"x-content-type-options", "nosniff"},
{"link", target_sxg_outer_link_header}}));
ScopedSignedExchangeHandlerFactory scoped_factory(&factory);
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path));
// Wait until all (main- and sub-resource) SXGs are prefetched.
while (GetCachedExchanges().size() < base::size(kTestCases) + 1) {
base::RunLoop run_loop;
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout());
run_loop.Run();
}
NavigateToURLAndWaitTitle(target_sxg_url, "done");
for (size_t i = 0; i < base::size(kTestCases); ++i) {
SCOPED_TRACE(base::StringPrintf("TestCase %zu", i));
EXPECT_EQ(
EvalJs(shell(), base::StringPrintf("results[%zu]", i)).ExtractString(),
kTestCases[i].expected_result);
}
}
INSTANTIATE_TEST_SUITE_P(
SignedExchangeSubresourcePrefetchBrowserTest,
SignedExchangeSubresourcePrefetchBrowserTest,
......
......@@ -497,18 +497,11 @@ void CorsURLLoader::SetCorsFlagIfNeeded() {
if (fetch_cors_flag_)
return;
if (request_.fetch_request_mode == mojom::FetchRequestMode::kNavigate ||
request_.fetch_request_mode == mojom::FetchRequestMode::kNoCors) {
if (!network::cors::ShouldCheckCors(request_.url, request_.request_initiator,
request_.fetch_request_mode)) {
return;
}
if (request_.url.SchemeIs(url::kDataScheme))
return;
// CORS needs a proper origin (including a unique opaque origin). If the
// request doesn't have one, CORS should not work.
DCHECK(request_.request_initiator);
// The source origin and destination URL pair may be in the allow list.
switch (origin_access_list_->CheckAccessState(*request_.request_initiator,
request_.url)) {
......@@ -541,11 +534,6 @@ void CorsURLLoader::SetCorsFlagIfNeeded() {
return;
}
if (request_.request_initiator->IsSameOriginWith(
url::Origin::Create(request_.url))) {
return;
}
fetch_cors_flag_ = true;
}
......
......@@ -224,6 +224,28 @@ base::Optional<CorsErrorStatus> CheckAccess(
return base::nullopt;
}
bool ShouldCheckCors(const GURL& request_url,
const base::Optional<url::Origin>& request_initiator,
mojom::FetchRequestMode fetch_request_mode) {
if (fetch_request_mode == network::mojom::FetchRequestMode::kNavigate ||
fetch_request_mode == network::mojom::FetchRequestMode::kNoCors) {
return false;
}
// CORS needs a proper origin (including a unique opaque origin). If the
// request doesn't have one, CORS should not work.
DCHECK(request_initiator);
// TODO(crbug.com/870173): Remove following scheme check once the network
// service is fully enabled.
if (request_url.SchemeIs(url::kDataScheme))
return false;
if (request_initiator->IsSameOriginWith(url::Origin::Create(request_url)))
return false;
return true;
}
base::Optional<CorsErrorStatus> CheckPreflightAccess(
const GURL& response_url,
const int response_status_code,
......
......@@ -58,6 +58,14 @@ base::Optional<CorsErrorStatus> CheckAccess(
mojom::FetchCredentialsMode credentials_mode,
const url::Origin& origin);
// Returns true if |fetch_request_mode| is not kNavigate nor kNoCors, and the
// origin of |request_url| is not a data URL, and |request_initiator| is not
// same as the origin of |request_url|,
COMPONENT_EXPORT(NETWORK_CPP)
bool ShouldCheckCors(const GURL& request_url,
const base::Optional<url::Origin>& request_initiator,
mojom::FetchRequestMode fetch_request_mode);
// Performs a CORS access check on the CORS-preflight response parameters.
// According to the note at https://fetch.spec.whatwg.org/#cors-preflight-fetch
// step 6, even for a preflight check, |credentials_mode| should be checked on
......
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