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 <chikamune@chromium.org>
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 <chikamune@chromium.org>
Reviewed-by: Kent Tamura <tkent@chromium.org>
Reviewed-by: Takashi Toyoshima <toyoshim@chromium.org>
Reviewed-by: Shunya Shishido <sisidovski@chromium.org>
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