Low chrome UAF 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Loader
DescriptionUse after free in Loader
ComponentLoader
Bug ClassUAF
Tracker497827101
Fix commitb47df211a2c8 (chromium/src) +647/-53
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
services/network/public/cpp/sri_message_signatures.cc
modified

Files Changed

  • content/common/service_worker/race_network_request_url_loader_client_unittest.cc
  • content/renderer/service_worker/service_worker_subresource_loader.h
  • services/network/public/cpp/sri_message_signatures.cc
  • services/network/public/cpp/sri_message_signatures_unittest.cc
  • content/common/service_worker/service_worker_resource_loader_unittest.cc
  • content/browser/service_worker/service_worker_main_resource_loader.cc
  • content/renderer/service_worker/service_worker_subresource_loader.cc
  • content/renderer/service_worker/service_worker_subresource_loader_unittest.cc
  • content/browser/service_worker/service_worker_main_resource_loader_unittest.cc
  • content/test/content_test_bundle_data.filelist
  • content/test/data/service_worker/synthetic_integrity_response_worker.js
  • third_party/blink/common/service_worker/service_worker_loader_helpers.cc
  • third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py
  • content/common/service_worker/service_worker_resource_loader.h
  • services/network/public/cpp/sri_message_signatures.h
  • content/browser/service_worker/service_worker_main_resource_loader.h
  • content/browser/service_worker/service_worker_browsertest.cc
  • third_party/blink/common/service_worker/service_worker_loader_helpers_unittest.cc
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,
                                    &param_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.