Chrome · Loader
CVE-2026-113229
UAF in Loader
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifservices/network/public/cpp/sri_message_signatures.cc |
modified |
Files Changed
content/common/service_worker/race_network_request_url_loader_client_unittest.cccontent/renderer/service_worker/service_worker_subresource_loader.hservices/network/public/cpp/sri_message_signatures.ccservices/network/public/cpp/sri_message_signatures_unittest.cccontent/common/service_worker/service_worker_resource_loader_unittest.cccontent/browser/service_worker/service_worker_main_resource_loader.cccontent/renderer/service_worker/service_worker_subresource_loader.cccontent/renderer/service_worker/service_worker_subresource_loader_unittest.cccontent/browser/service_worker/service_worker_main_resource_loader_unittest.cccontent/test/content_test_bundle_data.filelistcontent/test/data/service_worker/synthetic_integrity_response_worker.jsthird_party/blink/common/service_worker/service_worker_loader_helpers.ccthird_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.pycontent/common/service_worker/service_worker_resource_loader.hservices/network/public/cpp/sri_message_signatures.hcontent/browser/service_worker/service_worker_main_resource_loader.hcontent/browser/service_worker/service_worker_browsertest.ccthird_party/blink/common/service_worker/service_worker_loader_helpers_unittest.cc
Patch
From b47df211a2c8f4723adf31ba5ac8fa017b39d74b Mon Sep 17 00:00:00 2001 From: Minoru Chikamune <[email protected]> Date: Thu, 24 Sep 2026 00:07:23 -0700 Subject: [PATCH] Enforce SRI message signatures for Service Worker responses Currently, Subresource Integrity (SRI) using Ed25519 message signatures relies on signature validation within the network service (URLLoader). However, if a Service Worker synthesizes a response (e.g. using respondWith(new Response(...))), the network service is bypassed, allowing a forged signature to bypass integrity checks in Blink, which only matches the keyid. This CL resolves the issue by: 1. Explicitly invoking MaybeBlockResponseForSRIMessageSignature inside ServiceWorkerMainResourceLoader and ServiceWorkerSubresourceLoader, ordered after redirect handling to properly evaluate final responses. 2. Failing closed in MaybeBlockResponseForSRIMessageSignature when expected_public_keys is asserted and headers, signatures, or valid URLs are missing. 3. Permitting Unencoded-Digest, Signature, and Signature-Input in IsCorsExposedResponseHeader so that CORS-filtered synthetic responses retain headers required for browser and loader-level SRI verification. TAG=agy CONV=7c174f44-f6b2-433a-b5b9-ed214e64bb89 Fixed: 497827101 Change-Id: Ie5d4416b0e1fa761bed7704b753e2fd5e8596f1a Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8260651 Commit-Queue: Minoru Chikamune <[email protected]> Reviewed-by: Kent Tamura <[email protected]> Reviewed-by: Takashi Toyoshima <[email protected]> Reviewed-by: Shunya Shishido <[email protected]> Cr-Commit-Position: refs/heads/main@{#1704074} --- diff --git a/content/browser/service_worker/service_worker_main_resource_loader.cc b/content/browser/service_worker/service_worker_main_resource_loader.cc index f12963d..c45fa41 100644 --- a/content/browser/service_worker/service_worker_main_resource_loader.cc +++ b/content/browser/service_worker/service_worker_main_resource_loader.cc @@ -54,6 +54,7 @@ #include "services/network/public/cpp/cross_origin_embedder_policy.h" #include "services/network/public/cpp/document_isolation_policy.h" #include "services/network/public/cpp/features.h" +#include "services/network/public/cpp/sri_message_signatures.h" #include "services/network/public/cpp/timing_allow_origin_parser.h" #include "services/network/public/mojom/fetch_api.mojom.h" #include "services/network/public/mojom/service_worker_router_info.mojom.h" @@ -743,8 +744,11 @@ CommitCompleted(net::OK, "No body exists."); } -void ServiceWorkerMainResourceLoader::CommitCompleted(int error_code, - const char* reason) { +void ServiceWorkerMainResourceLoader::CommitCompleted( + int error_code, + const char* reason, + std::optional<network::mojom::BlockedByResponseReason> + blocked_by_response_reason) { TRACE_EVENT("ServiceWorker", "ServiceWorkerMainResourceLoader::CommitCompleted", perfetto::Flow::FromPointer(this), "error_code", @@ -773,8 +777,9 @@ // |stream_waiter_| calls this when done. stream_waiter_.reset(); - url_loader_client_->OnComplete( - network::URLLoaderCompletionStatus(error_code)); + network::URLLoaderCompletionStatus status(error_code); + status.blocked_by_response_reason = blocked_by_response_reason; + url_loader_client_->OnComplete(status); } void ServiceWorkerMainResourceLoader::DidPrepareFetchEvent( @@ -1409,6 +1414,16 @@ return; } + // SRI verification applies to the final response, not redirect hops. + if (std::optional<network::mojom::BlockedByResponseReason> blocked_reason = + network::MaybeBlockResponseForSRIMessageSignature( + resource_request_, *response_head_, + resource_request_.expected_public_keys)) { + CommitCompleted(net::ERR_BLOCKED_BY_RESPONSE, + "SRI message signature mismatch", blocked_reason); + return; + } + CHECK(url_loader_client_.is_bound()); // Handle a stream response body. diff --git a/content/browser/service_worker/service_worker_main_resource_loader.h b/content/browser/service_worker/service_worker_main_resource_loader.h index be0fe6f..9e2d37e 100644 --- a/content/browser/service_worker/service_worker_main_resource_loader.h +++ b/content/browser/service_worker/service_worker_main_resource_loader.h @@ -6,6 +6,7 @@ #define CONTENT_BROWSER_SERVICE_WORKER_SERVICE_WORKER_MAIN_RESOURCE_LOADER_H_ #include <memory> +#include <optional> #include <string> #include <vector> @@ -162,9 +163,13 @@ // Sends net::ERR_INSUFFICIENT_RESOURCES when it can't be created. void CommitEmptyResponseAndComplete() override; + using ServiceWorkerResourceLoader::CommitCompleted; // Calls url_loader_client_->OnComplete(). |reason| will be recorded as an // argument of TRACE_EVENT. - void CommitCompleted(int error_code, const char* reason) override; + void CommitCompleted(int error_code, + const char* reason, + std::optional<network::mojom::BlockedByResponseReason> + blocked_by_response_reason) override; // Calls url_loader_client_->OnReceiveRedirect(). void HandleRedirect( diff --git a/content/common/service_worker/service_worker_resource_loader.h b/content/common/service_worker/service_worker_resource_loader.h index fec061e..8e8be4d2c 100644 --- a/content/common/service_worker/service_worker_resource_loader.h +++ b/content/common/service_worker/service_worker_resource_loader.h @@ -12,6 +12,7 @@ #include "mojo/public/cpp/base/big_buffer.h" #include "services/network/public/cpp/cross_origin_embedder_policy.h" #include "services/network/public/cpp/document_isolation_policy.h" +#include "services/network/public/mojom/blocked_by_response_reason.mojom-forward.h" #include "services/network/public/mojom/cross_origin_embedder_policy.mojom-forward.h" #include "services/network/public/mojom/document_isolation_policy.mojom-forward.h" #include "services/network/public/mojom/service_worker_router_info.mojom-shared.h" @@ -144,7 +145,14 @@ // Calls url_loader_client_->OnComplete(). |reason| will be recorded as an // argument of TRACE_EVENT. - virtual void CommitCompleted(int error_code, const char* reason) = 0; + void CommitCompleted(int error_code, const char* reason) { + CommitCompleted(error_code, reason, std::nullopt); + } + virtual void CommitCompleted( + int error_code, + const char* reason, + std::optional<network::mojom::BlockedByResponseReason> + blocked_by_response_reason) = 0; // Calls url_loader_client_->OnReceiveRedirect(). virtual void HandleRedirect( diff --git a/content/renderer/service_worker/service_worker_subresource_loader.cc b/content/renderer/service_worker/service_worker_subresource_loader.cc index f6a8219..6152137 100644 --- a/content/renderer/service_worker/service_worker_subresource_loader.cc +++ b/content/renderer/service_worker/service_worker_subresource_loader.cc @@ -33,6 +33,7 @@ #include "services/network/public/cpp/record_ontransfersizeupdate_utils.h" #include "services/network/public/cpp/resource_request.h" #include "services/network/public/cpp/shared_url_loader_factory.h" +#include "services/network/public/cpp/sri_message_signatures.h" #include "services/network/public/cpp/timing_allow_origin_parser.h" #include "services/network/public/mojom/early_hints.mojom.h" #include "services/network/public/mojom/service_worker_router_info.mojom.h" @@ -910,6 +911,16 @@ return; } + // SRI verification applies to the final response, not redirect hops. + if (std::optional<network::mojom::BlockedByResponseReason> blocked_reason = + network::MaybeBlockResponseForSRIMessageSignature( + resource_request_, *response_head_, + resource_request_.expected_public_keys)) { + CommitCompleted(net::ERR_BLOCKED_BY_RESPONSE, + "SRI message signature mismatch", blocked_reason); + return; + } + CHECK(url_loader_client_.is_bound()); bool body_stream_is_valid = !body_as_stream.is_null() && body_as_stream->stream.is_valid(); @@ -1013,8 +1024,11 @@ CommitCompleted(net::OK, "No body exists"); } -void ServiceWorkerSubresourceLoader::CommitCompleted(int error_code, - const char* reason) { +void ServiceWorkerSubresourceLoader::CommitCompleted( + int error_code, + const char* reason, + std::optional<network::mojom::BlockedByResponseReason> + blocked_by_response_reason) { TRACE_EVENT("ServiceWorker", "ServiceWorkerSubresourceLoader::CommitCompleted", perfetto::TerminatingFlow::Global( @@ -1044,6 +1058,7 @@ network::URLLoaderCompletionStatus status; status.error_code = error_code; status.completion_time = base::TimeTicks::Now(); + status.blocked_by_response_reason = blocked_by_response_reason; url_loader_client_->OnComplete(status); // Invalidate weak pointers to prevent callbacks after commit. This can diff --git a/content/renderer/service_worker/service_worker_subresource_loader.h b/content/renderer/service_worker/service_worker_subresource_loader.h index fbe7e5e..37a56a53 100644 --- a/content/renderer/service_worker/service_worker_subresource_loader.h +++ b/content/renderer/service_worker/service_worker_subresource_loader.h @@ -25,6 +25,7 @@ #include "net/traffic_annotation/network_traffic_annotation.h" #include "net/url_request/redirect_info.h" #include "services/network/public/cpp/resource_request.h" +#include "services/network/public/mojom/blocked_by_response_reason.mojom-forward.h" #include "services/network/public/mojom/url_loader.mojom.h" #include "services/network/public/mojom/url_loader_factory.mojom.h" #include "services/network/public/mojom/url_response_head.mojom-forward.h" @@ -154,9 +155,13 @@ // Sends net::ERR_INSUFFICIENT_RESOURCES when it can't be created. void CommitEmptyResponseAndComplete() override; - // Calls url_loader_client_->OnComplete(). Expected to be called after - // CommitResponseHeaders (i.e. status_ == kSentHeader). - void CommitCompleted(int error_code, const char* reason) override; + using ServiceWorkerResourceLoader::CommitCompleted; + // Calls url_loader_client_->OnComplete(). |reason| will be recorded as an + // argument of TRACE_EVENT. + void CommitCompleted(int error_code, + const char* reason, + std::optional<network::mojom::BlockedByResponseReason> + blocked_by_response_reason) override; // Calls url_loader_client_->OnReceiveRedirect(). Sends too many redirects // error if it hits the redirect limit. diff --git a/services/network/public/cpp/sri_message_signatures.cc b/services/network/public/cpp/sri_message_signatures.cc index 18ab39d..a8b94178 100644 --- a/services/network/public/cpp/sri_message_signatures.cc +++ b/services/network/public/cpp/sri_message_signatures.cc @@ -14,10 +14,12 @@ #include "base/strings/string_util.h" #include "base/types/optional_util.h" #include "net/base/url_util.h" +#include "net/http/http_request_headers.h" #include "net/http/http_util.h" #include "net/http/structured_headers.h" #include "net/url_request/url_request.h" #include "services/network/public/cpp/features.h" +#include "services/network/public/cpp/resource_request.h" #include "third_party/boringssl/src/include/openssl/curve25519.h" #include "url/gurl.h" @@ -324,22 +326,22 @@ } std::string SerializeDerivedComponent( - const net::URLRequest& url_request, + const GURL& request_url, + std::string_view request_method, const int response_status_code, const mojom::SRIMessageSignatureComponentPtr& component) { DCHECK(std::ranges::contains(kDerivedComponents, component->name)); - DCHECK(url_request.url().is_valid()); + DCHECK(request_url.is_valid()); if (component->name == "@authority") { // https://www.rfc-editor.org/rfc/rfc9421.html#name-authority - if (url_request.url().has_port()) { - return base::StrCat( - {url_request.url().host(), ":", url_request.url().port()}); + if (request_url.has_port()) { + return base::StrCat({request_url.host(), ":", request_url.port()}); } - return url_request.url().GetHost(); + return request_url.GetHost(); } else if (component->name == "@query") { // https://www.rfc-editor.org/rfc/rfc9421.html#name-query - return base::StrCat({"?", url_request.url().GetQuery()}); + return base::StrCat({"?", request_url.GetQuery()}); } else if (component->name == "@query-param") { DCHECK(component->params.size() == 2u); auto name_it = @@ -349,19 +351,19 @@ }); DCHECK(name_it != component->params.end() && (*name_it)->value.has_value()); std::string param_value; - if (net::GetValueForKeyInQuery(url_request.url(), *(*name_it)->value, + if (net::GetValueForKeyInQuery(request_url, *(*name_it)->value, ¶m_value)) { return base::EscapeAllExceptUnreserved(param_value); } return std::string(); } else if (component->name == "@method") { // https://www.rfc-editor.org/rfc/rfc9421.html#content-request-method - return url_request.method(); + return std::string(request_method); } else if (component->name == "@path") { // https://www.rfc-editor.org/rfc/rfc9421.html#content-request-path - return url_request.url().GetPath(); + return request_url.GetPath(); } else if (component->name == "@scheme") { - return url_request.url().GetScheme(); + return request_url.GetScheme(); } else if (component->name == "@status") { // https://www.rfc-editor.org/rfc/rfc9421.html#content-status-code return base::NumberToString(response_status_code); @@ -372,7 +374,7 @@ // those components as well, just as we do for referrers. // // https://datatracker.ietf.org/doc/html/rfc9421#content-target-uri - return url_request.url().GetAsReferrer().spec(); + return request_url.GetAsReferrer().spec(); } // TODO(383409584): Support additional derived components. @@ -687,11 +689,14 @@ return parsed_headers; } +namespace { base::expected<std::string, mojom::SRIMessageSignatureError> -ConstructSignatureBase(const mojom::SRIMessageSignaturePtr& signature, - const net::URLRequest& url_request, - const net::HttpResponseHeaders& headers) { - DCHECK(url_request.url().is_valid()); +ConstructSignatureBaseImpl(const mojom::SRIMessageSignaturePtr& signature, + const GURL& request_url, + std::string_view request_method, + const net::HttpRequestHeaders& request_headers, + const net::HttpResponseHeaders& headers) { + DCHECK(request_url.is_valid()); if (!signature) { return base::unexpected( @@ -736,7 +741,7 @@ kSignatureBaseUnknownDerivedComponent); } component_value = SerializeDerivedComponent( - url_request, headers.response_code(), component); + request_url, request_method, headers.response_code(), component); // * If the component name does not start with an "at" (`@`) // character, canonizalize the HTTP field value ... If the field @@ -747,7 +752,7 @@ // out of signature base generation if the header isn't present std::optional<std::string> header = IsRequestComponent(component) - ? url_request.extra_request_headers().GetHeader(component->name) + ? request_headers.GetHeader(component->name) : headers.GetNormalizedHeader(component->name); if (!header.has_value()) { return base::unexpected( @@ -803,11 +808,13 @@ return result; } -bool ValidateSRIMessageSignaturesOverHeaders( +bool ValidateSRIMessageSignaturesOverHeadersImpl( mojom::SRIMessageSignaturesPtr& message_signatures, - const net::URLRequest& url_request, + const GURL& request_url, + std::string_view request_method, + const net::HttpRequestHeaders& request_headers, const net::HttpResponseHeaders& headers) { - DCHECK(url_request.url().is_valid()); + DCHECK(request_url.is_valid()); // If no signatures are present, validation automatically succeeds. if (message_signatures->signatures.empty()) { @@ -829,8 +836,9 @@ // Generate the signature base: base::expected<std::string, mojom::SRIMessageSignatureError> - signature_base_result = - ConstructSignatureBase(message_signature, url_request, headers); + signature_base_result = ConstructSignatureBaseImpl( + message_signature, request_url, request_method, request_headers, + headers); if (!signature_base_result.has_value()) { AddIssueFromErrorEnum(signature_base_result.error(), message_signatures->issues); @@ -869,23 +877,34 @@ } std::optional<mojom::BlockedByResponseReason> -MaybeBlockResponseForSRIMessageSignature( - const net::URLRequest& url_request, +MaybeBlockResponseForSRIMessageSignatureImpl( + const GURL& request_url, + std::string_view request_method, + const net::HttpRequestHeaders& request_headers, const network::mojom::URLResponseHead& response, const std::vector<std::vector<uint8_t>>& expected_public_keys, - const raw_ptr<mojom::DevToolsObserver> devtools_observer, + mojom::DevToolsObserver* devtools_observer, const std::string& devtools_request_id) { - // No headers, no URL: no blocking. - const GURL& request_url = url_request.url(); + const bool integrity_required = !expected_public_keys.empty(); + + // If integrity verification is required, missing headers or an invalid URL + // must fail closed. Otherwise, there is nothing to enforce or block. if (!response.headers || !request_url.is_valid()) { - return std::nullopt; + return integrity_required + ? std::make_optional(mojom::BlockedByResponseReason:: + kSRIMessageSignatureMismatch) + : std::nullopt; } + ... (file diff truncated)
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/content/browser/service_worker/service_worker_browsertest.cc b/content/browser/service_worker/service_worker_browsertest.cc
index f34c70d3..291c5b5 100644
--- a/content/browser/service_worker/service_worker_browsertest.cc
+++ b/content/browser/service_worker/service_worker_browsertest.cc
@@ -701,6 +701,79 @@
run_loop.Run();
}
+IN_PROC_BROWSER_TEST_F(ServiceWorkerBrowserTest,
+ SubresourceIntegrityRobustness) {
+ StartServerAndNavigateToSetup();
+ const char kPageUrl[] = "/service_worker/empty.html";
+ const char kWorkerUrl[] =
+ "/service_worker/synthetic_integrity_response_worker.js";
+ WorkerStateObserver observer(wrapper(), ServiceWorkerVersion::ACTIVATED);
+ blink::mojom::ServiceWorkerRegistrationOptions options(
+ embedded_test_server()->GetURL(kPageUrl),
+ blink::mojom::ScriptType::kClassic,
+ blink::mojom::ServiceWorkerUpdateViaCache::kImports);
+ const blink::StorageKey key =
+ blink::StorageKey::CreateFirstParty(url::Origin::Create(options.scope));
+ public_context()->RegisterServiceWorker(
+ embedded_test_server()->GetURL(kWorkerUrl), key, options,
+ GlobalRenderFrameHostId(),
+ base::BindOnce(&ExpectRegisterResultAndRun,
+ blink::ServiceWorkerStatusCode::kOk, base::DoNothing()));
+ observer.Wait();
+
+ EXPECT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(kPageUrl)));
+
+ // When requested without integrity requirements, the synthetic response
+ // loads.
+ const char kLoadWithoutIntegrityScript[] = R"(
+ new Promise(resolve => {
+ const script = document.createElement('script');
+ script.src = '/service_worker/integrity_test.js';
+ script.onload = () => resolve('loaded');
+ script.onerror = () => resolve('error');
+ document.body.appendChild(script);
+ });
+ )";
+ EXPECT_EQ("loaded", EvalJs(shell(), kLoadWithoutIntegrityScript));
+
+ // When requested with integrity attributes, invalid synthetic signatures
+ // must be rejected.
+ const char kLoadWithIntegrityScript[] = R"(
+ new Promise(resolve => {
+ const script = document.createElement('script');
+ script.src = '/service_worker/integrity_test.js?with_sri';
+ script.integrity = 'ed25519-JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=';
+ script.onload = () => resolve('loaded');
+ script.onerror = () => resolve('error');
+ document.body.appendChild(script);
+ });
+ )";
+ EXPECT_EQ("error", EvalJs(shell(), kLoadWithIntegrityScript));
+
+ // When requested with matching integrity attributes, valid synthetic
+ // signatures must be accepted, and the script must execute end-to-end.
+ const char kLoadWithValidIntegrityScript[] = R"(
+ new Promise(resolve => {
+ const script = document.createElement('script');
+ script.src = '/service_worker/integrity_test.js?with_valid_sri';
+ script.integrity = 'ed25519-JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=';
+ script.onload = () => resolve('loaded');
+ script.onerror = () => resolve('error');
+ document.body.appendChild(script);
+ });
+ )";
+ EXPECT_EQ("loaded", EvalJs(shell(), kLoadWithValidIntegrityScript));
+ EXPECT_EQ(true, EvalJs(shell(), "window.__sri_service_worker_executed"));
+
+ base::RunLoop run_loop;
+ public_context()->UnregisterServiceWorker(
+ embedded_test_server()->GetURL(kPageUrl), key,
+ base::BindOnce(&ExpectUnregisterResultAndRun,
+ blink::ServiceWorkerStatusCode::kOk,
+ run_loop.QuitClosure()));
+ run_loop.Run();
+}
+
// Tests that when data saver is enabled and a cross-origin fetch by a webpage
// is intercepted by a serviceworker, and the serviceworker does a fetch, the
// preflight request does not have save-data in Access-Control-Request-Headers.
diff --git a/content/browser/service_worker/service_worker_main_resource_loader_unittest.cc b/content/browser/service_worker/service_worker_main_resource_loader_unittest.cc
index d10b32c1..2fb31229 100644
--- a/content/browser/service_worker/service_worker_main_resource_loader_unittest.cc
+++ b/content/browser/service_worker/service_worker_main_resource_loader_unittest.cc
@@ -7,6 +7,7 @@
#include <string>
#include <utility>
+#include "base/base64.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
@@ -52,6 +53,7 @@
#include "services/network/public/cpp/single_request_url_loader_factory.h"
#include "services/network/public/cpp/timing_allow_origin_parser.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
+#include "services/network/public/mojom/blocked_by_response_reason.mojom.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/public/mojom/service_worker_router_info.mojom-shared.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
@@ -60,6 +62,7 @@
#include "storage/browser/blob/blob_data_handle.h"
#include "storage/browser/blob/blob_impl.h"
#include "storage/browser/blob/blob_storage_context.h"
+#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
@@ -2611,5 +2614,29 @@
/*expected_timing_allow_passed=*/false,
/*response_timing_allow_passed=*/false}));
+TEST_F(ServiceWorkerMainResourceLoaderTest, SRIMessageSignatureRejected) {
+ service_worker_->RespondWithHeaders(
+ {{"Signature",
+ "sig=:"
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ "AAAAAAAAAAAAAAAAAAAA=:"},
+ {"Signature-Input",
+ "sig=(\"unencoded-digest\";sf);keyid=\"JrQLj5P/"
+ "89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=\";tag=\"ed25519-integrity\""},
+ {"Unencoded-Digest",
+ "sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:"}});
+
+ std::unique_ptr<network::ResourceRequest> request = CreateRequest();
+ request->expected_public_keys = {
+ *base::Base64Decode("JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=")};
+ StartRequest(std::move(request));
+ client_.RunUntilComplete();
+
+ EXPECT_EQ(net::ERR_BLOCKED_BY_RESPONSE,
+ client_.completion_status().error_code);
+ EXPECT_THAT(client_.completion_status().blocked_by_response_reason,
+ testing::Optional(network::mojom::BlockedByResponseReason::
+ kSRIMessageSignatureMismatch));
+}
} // namespace service_worker_main_resource_loader_unittest
} // namespace content
diff --git a/content/common/service_worker/race_network_request_url_loader_client_unittest.cc b/content/common/service_worker/race_network_request_url_loader_client_unittest.cc
index 242b2e45..01c30168 100644
--- a/content/common/service_worker/race_network_request_url_loader_client_unittest.cc
+++ b/content/common/service_worker/race_network_request_url_loader_client_unittest.cc
@@ -83,7 +83,11 @@
}
}
void CommitEmptyResponseAndComplete() override {}
- void CommitCompleted(int error_code, const char* reason) override {
+ using ServiceWorkerResourceLoader::CommitCompleted;
+ void CommitCompleted(int error_code,
+ const char* reason,
+ std::optional<network::mojom::BlockedByResponseReason>
+ blocked_by_response_reason) override {
std::move(on_commit_completed_).Run(error_code, reason);
}
void HandleRedirect(
diff --git a/content/common/service_worker/service_worker_resource_loader_unittest.cc b/content/common/service_worker/service_worker_resource_loader_unittest.cc
index 7803e62..03a99aea 100644
--- a/content/common/service_worker/service_worker_resource_loader_unittest.cc
+++ b/content/common/service_worker/service_worker_resource_loader_unittest.cc
@@ -32,7 +32,11 @@
void CommitEmptyResponseAndComplete() override {}
- void CommitCompleted(int error_code, const char* reason) override {}
+ using ServiceWorkerResourceLoader::CommitCompleted;
+ void CommitCompleted(int error_code,
+ const char* reason,
+ std::optional<network::mojom::BlockedByResponseReason>
+ blocked_by_response_reason) override {}
void HandleRedirect(
const net::RedirectInfo& redirect_info,
diff --git a/content/renderer/service_worker/service_worker_subresource_loader_unittest.cc b/content/renderer/service_worker/service_worker_subresource_loader_unittest.cc
index ebfcfa3b..7c3c89b5 100644
--- a/content/renderer/service_worker/service_worker_subresource_loader_unittest.cc
+++ b/content/renderer/service_worker/service_worker_subresource_loader_unittest.cc
@@ -10,6 +10,7 @@
#include <utility>
#include <vector>
+#include "base/base64.h"
#include "base/containers/span.h"
#include "base/debug/stack_trace.h"
#include "base/functional/bind.h"
@@ -35,9 +36,11 @@
#include "net/url_request/url_request.h"
#include "services/network/public/cpp/timing_allow_origin_parser.h"
#include "services/network/public/cpp/wrapper_shared_url_loader_factory.h"
+#include "services/network/public/mojom/blocked_by_response_reason.mojom.h"
#include "services/network/test/test_data_pipe_getter.h"
#include "services/network/test/test_url_loader_client.h"
#include "services/network/test/test_url_loader_factory.h"
+#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/service_worker/service_worker_router_rule.h"
#include "third_party/blink/public/mojom/blob/blob.mojom.h"
@@ -220,6 +223,15 @@
blob_body_->blob.InitWithNewPipeAndPassReceiver());
}
+ void RespondWithBlobAndHeaders(
+ std::optional<std::vector<uint8_t>> metadata,
+ std::string body,
+ std::vector<std::pair<std::string, std::string>> headers) {
+ RespondWithBlob(std::move(metadata), std::move(body));
+ response_mode_ = ResponseMode::kBlobWithHeaders;
+ extra_headers_ = std::move(headers);
+ }
+
// Tells this controller to respond to fetch events with a 206 partial
// response, returning a blob composed of the requested bytes of |body|
// according to the request headers.
@@ -334,13 +346,22 @@
blink::mojom::ServiceWorkerEventStatus::COMPLETED);
break;
case ResponseMode::kBlob:
- response_callback->OnResponse(
+ case ResponseMode::kBlobWithHeaders: {
+ auto response =
OkResponse(std::move(blob_body_), response_source_, response_time_,
- cache_storage_cache_name_, response_type_),
- std::move(timing), /*errors=*/nullptr);
+ cache_storage_cache_name_, response_type_,
+ parsed_headers_ ? parsed_headers_->Clone() : nullptr);
+ if (response_mode_ == ResponseMode::kBlobWithHeaders) {
+ for (const auto& pair : extra_headers_) {
+ response->headers.emplace(pair.first, pair.second);
+ }
+ }
+ response_callback->OnResponse(std::move(response), std::move(timing),
+ /*errors=*/nullptr);
std::move(callback).Run(
blink::mojom::ServiceWorkerEventStatus::COMPLETED);
break;
+ }
case ResponseMode::kBlobRange: {
// Parse the Range header.
@@ -438,10 +459,12 @@
kFallbackResponse,
kErrorResponse,
kRedirectResponse,
- kDontRespond
+ kDontRespond,
+ kBlobWithHeaders
};
ResponseMode response_mode_ = ResponseMode::kDefault;
+ std::vector<std::pair<std::string, std::string>> extra_headers_;
scoped_refptr<network::ResourceRequestBody> request_body_;
int fetch_event_count_ = 0;
@@ -1838,5 +1861,87 @@
}
}
+TEST_F(ServiceWorkerSubresourceLoaderTest, SRIMessageSignatureRejected) {
+ fake_controller_.RespondWithBlobAndHeaders(
+ std::nullopt, "sample text",
+ {{"Signature",
+ "sig=:"
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ "AAAAAAAAAAAAAAAAAAAA=:"},
+ {"Signature-Input",
+ "sig=(\"unencoded-digest\";sf);keyid=\"JrQLj5P/"
+ "89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=\";tag=\"ed25519-integrity\""},
+ {"Unencoded-Digest",
+ "sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:"}});
+
+ mojo::Remote<network::mojom::URLLoaderFactory> factory =
+ CreateSubresourceLoaderFactory();
+
+ network::ResourceRequest request =
+ CreateRequest(GURL("https://www.example.com/foo.js"));
+ request.expected_public_keys = {
+ *base::Base64Decode("JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=")};
+ mojo::Remote<network::mojom::URLLoader> loader;
+ std::unique_ptr<network::TestURLLoaderClient> client;
+ StartRequest(factory, request, &loader, &client);
+ client->RunUntilComplete();
+
+ EXPECT_EQ(net::ERR_BLOCKED_BY_RESPONSE,
+ client->completion_status().error_code);
+ EXPECT_THAT(client->completion_status().blocked_by_response_reason,
+ testing::Optional(network::mojom::BlockedByResponseReason::
+ kSRIMessageSignatureMismatch));
+}
+
+TEST_F(ServiceWorkerSubresourceLoaderTest, SRIMessageSignatureAccepted) {
+ fake_controller_.RespondWithBlobAndHeaders(
+ std::nullopt, "sample text",
+ {{"Signature",
+ "signature=:SbCdPUyjc0IBJjFbVRWs81ucEUcFz87b37nQ63d6kDW+/"
+ "JvDmET6O5cSdwlddePvlwemLdaWFuY6pQGO+hrkAg==:"},
+ {"Signature-Input",
+ "signature=(\"unencoded-digest\";sf);keyid=\"JrQLj5P/"
... (truncated)
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page