CVE-2026-13876
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifservices/network/public/cpp/sri_message_signatures.cc |
modified | |
TEST_Fservices/network/public/cpp/sri_message_signatures_unittest.cc |
modified |
Files Changed
content/browser/devtools/network_service_devtools_observer.ccservices/network/public/cpp/sri_message_signatures.ccservices/network/public/cpp/sri_message_signatures.hservices/network/public/cpp/sri_message_signatures_unittest.cc
Patch
From 90447b94c751c23d5d2589e072a5f1cb156bb7b3 Mon Sep 17 00:00:00 2001 From: Mike West <[email protected]> Date: Tue, 12 May 2026 00:30:49 -0700 Subject: [PATCH] [SRI Message Signatures] Fail validation when signature base fails. Rather than synthesizing an empty string when signature base construction fails, we ought to fail validation entirely. This CL does that, and adds additional error enum values to cover the ways in which signature base generation might fail. Bug: 498722200 Change-Id: Ia1f27540626400765a94017389472b02cda5bef1 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7826651 Reviewed-by: Danil Somsikov <[email protected]> Reviewed-by: Kenichi Ishibashi <[email protected]> Commit-Queue: Mike West <[email protected]> Cr-Commit-Position: refs/heads/main@{#1629106} --- diff --git a/content/browser/devtools/network_service_devtools_observer.cc b/content/browser/devtools/network_service_devtools_observer.cc index 694c283..4c724c8 100644 --- a/content/browser/devtools/network_service_devtools_observer.cc +++ b/content/browser/devtools/network_service_devtools_observer.cc @@ -423,6 +423,14 @@ return SRIMessageSignatureErrorEnum::ValidationFailedInvalidLength; case SRIMessageSignatureError::kValidationFailedIntegrityMismatch: return SRIMessageSignatureErrorEnum::ValidationFailedIntegrityMismatch; + case SRIMessageSignatureError::kSignatureBaseUnknownDerivedComponent: + return SRIMessageSignatureErrorEnum::SignatureBaseUnknownDerivedComponent; + case SRIMessageSignatureError::kSignatureBaseMissingHeader: + return SRIMessageSignatureErrorEnum::SignatureBaseMissingHeader; + case SRIMessageSignatureError::kSignatureBaseInvalidUnencodedDigest: + return SRIMessageSignatureErrorEnum::SignatureBaseInvalidUnencodedDigest; + case SRIMessageSignatureError::kSignatureBaseUnsupportedComponent: + return SRIMessageSignatureErrorEnum::SignatureBaseUnsupportedComponent; } } diff --git a/services/network/public/cpp/sri_message_signatures.cc b/services/network/public/cpp/sri_message_signatures.cc index d515adec..350bfc04 100644 --- a/services/network/public/cpp/sri_message_signatures.cc +++ b/services/network/public/cpp/sri_message_signatures.cc @@ -665,15 +665,16 @@ return parsed_headers; } -std::optional<std::string> ConstructSignatureBase( - const mojom::SRIMessageSignaturePtr& signature, - const net::URLRequest& url_request, - const net::HttpResponseHeaders& headers) { +base::expected<std::string, mojom::SRIMessageSignatureError> +ConstructSignatureBase(const mojom::SRIMessageSignaturePtr& signature, + const net::URLRequest& url_request, + const net::HttpResponseHeaders& headers) { const GURL request_url = url_request.url(); DCHECK(request_url.is_valid()); if (!signature) { - return std::nullopt; + return base::unexpected( + mojom::SRIMessageSignatureError::kSignatureInputHeaderMissingLabel); } // Build the signature base per @@ -710,7 +711,8 @@ std::optional<std::string> component_value; if (component->name.starts_with('@')) { if (!std::ranges::contains(kDerivedComponents, component->name)) { - return std::nullopt; + return base::unexpected(mojom::SRIMessageSignatureError:: + kSignatureBaseUnknownDerivedComponent); } component_value = SerializeDerivedComponent( url_request, headers.response_code(), component); @@ -727,8 +729,8 @@ ? url_request.extra_request_headers().GetHeader(component->name) : headers.GetNormalizedHeader(component->name); if (!header.has_value()) { - // TODO(mkwst): We should have a more-specific error here. - return std::nullopt; + return base::unexpected( + mojom::SRIMessageSignatureError::kSignatureBaseMissingHeader); } // Determine how to serialize the header: @@ -746,12 +748,14 @@ std::optional<net::structured_headers::Dictionary> dict = net::structured_headers::ParseDictionary(header.value()); if (!dict.has_value()) { - return std::nullopt; + return base::unexpected(mojom::SRIMessageSignatureError:: + kSignatureBaseInvalidUnencodedDigest); } component_value = net::structured_headers::SerializeDictionary(dict.value()); } else { - return std::nullopt; + return base::unexpected(mojom::SRIMessageSignatureError:: + kSignatureBaseUnsupportedComponent); } } else { component_value = header.value(); @@ -760,7 +764,8 @@ // 2.6. Append the covered component's canonicalized component value. // 2.7. Append a single newline (`\n`). if (!component_value.has_value()) { - return std::nullopt; + return base::unexpected( + mojom::SRIMessageSignatureError::kSignatureBaseUnsupportedComponent); } signature_base << component_value.value() << '\n'; } @@ -803,9 +808,15 @@ } // Generate the signature base: - std::string signature_base = - ConstructSignatureBase(message_signature, url_request, headers) - .value_or(""); + base::expected<std::string, mojom::SRIMessageSignatureError> + signature_base_result = + ConstructSignatureBase(message_signature, url_request, headers); + if (!signature_base_result.has_value()) { + AddIssueFromErrorEnum(signature_base_result.error(), + message_signatures->issues); + return false; + } + const std::string& signature_base = signature_base_result.value(); // Decode the public key, and validate that both the public key and the // message's signature are the correct length for Ed25519 (32 and 64 bits, diff --git a/services/network/public/cpp/sri_message_signatures.h b/services/network/public/cpp/sri_message_signatures.h index f966806..e16941e 100644 --- a/services/network/public/cpp/sri_message_signatures.h +++ b/services/network/public/cpp/sri_message_signatures.h @@ -9,6 +9,7 @@ #include <vector> #include "base/component_export.h" +#include "base/types/expected.h" #include "net/http/http_response_headers.h" #include "services/network/public/mojom/blocked_by_response_reason.mojom.h" #include "services/network/public/mojom/devtools_observer.mojom.h" @@ -34,15 +35,14 @@ // Given an SRI Message Signature, a request, and a set of response headers, // construct the "signature base" as per Section 2.5 of RFC9421. Returns -// `std::nullopt` and populates `SRIMessageSignature::issues` if no base can -// be constructed. +// the signature base on success, or an error code on failure. // // https://www.rfc-editor.org/rfc/rfc9421.html#name-creating-the-signature-base COMPONENT_EXPORT(NETWORK_CPP) -std::optional<std::string> ConstructSignatureBase( - const mojom::SRIMessageSignaturePtr& signature, - const net::URLRequest& url_request, - const net::HttpResponseHeaders& headers); +base::expected<std::string, mojom::SRIMessageSignatureError> +ConstructSignatureBase(const mojom::SRIMessageSignaturePtr& signature, + const net::URLRequest& url_request, + const net::HttpResponseHeaders& headers); // Validates a response's SRI-relevant HTTP Message Signatures. // diff --git a/services/network/public/cpp/sri_message_signatures_unittest.cc b/services/network/public/cpp/sri_message_signatures_unittest.cc index 3db683a..e3907cb 100644 --- a/services/network/public/cpp/sri_message_signatures_unittest.cc +++ b/services/network/public/cpp/sri_message_signatures_unittest.cc @@ -838,9 +838,11 @@ net::HttpResponseHeaders::Builder(net::HttpVersion(1, 1), "200").Build(); mojom::SRIMessageSignaturePtr signature; - std::optional<std::string> result = + base::expected<std::string, mojom::SRIMessageSignatureError> result = ConstructSignatureBase(signature, request(), *headers); EXPECT_FALSE(result.has_value()); + EXPECT_EQ(mojom::SRIMessageSignatureError::kSignatureInputHeaderMissingLabel, + result.error()); } TEST_F(SRIMessageSignatureBaseTest, ValidHeadersValidBase) { @@ -851,7 +853,7 @@ ASSERT_EQ(1u, parsed->signatures.size()); EXPECT_EQ(0u, parsed->issues.size()); - std::optional<std::string> result = + base::expected<std::string, mojom::SRIMessageSignatureError> result = ConstructSignatureBase(parsed->signatures[0], request(), *headers); ASSERT_TRUE(result.has_value()); std::string expected_base = @@ -898,7 +900,7 @@ ASSERT_EQ(1u, parsed->signatures.size()); EXPECT_EQ(0u, parsed->issues.size()); - std::optional<std::string> result = + base::expected<std::string, mojom::SRIMessageSignatureError> result = ConstructSignatureBase(parsed->signatures[0], request(), *headers); ASSERT_TRUE(result.has_value()); std::string expected_base = @@ -946,7 +948,7 @@
Regression Test / PoC
diff --git a/services/network/public/cpp/sri_message_signatures_unittest.cc b/services/network/public/cpp/sri_message_signatures_unittest.cc
index 3db683a..e3907cb 100644
--- a/services/network/public/cpp/sri_message_signatures_unittest.cc
+++ b/services/network/public/cpp/sri_message_signatures_unittest.cc
@@ -838,9 +838,11 @@
net::HttpResponseHeaders::Builder(net::HttpVersion(1, 1), "200").Build();
mojom::SRIMessageSignaturePtr signature;
- std::optional<std::string> result =
+ base::expected<std::string, mojom::SRIMessageSignatureError> result =
ConstructSignatureBase(signature, request(), *headers);
EXPECT_FALSE(result.has_value());
+ EXPECT_EQ(mojom::SRIMessageSignatureError::kSignatureInputHeaderMissingLabel,
+ result.error());
}
TEST_F(SRIMessageSignatureBaseTest, ValidHeadersValidBase) {
@@ -851,7 +853,7 @@
ASSERT_EQ(1u, parsed->signatures.size());
EXPECT_EQ(0u, parsed->issues.size());
- std::optional<std::string> result =
+ base::expected<std::string, mojom::SRIMessageSignatureError> result =
ConstructSignatureBase(parsed->signatures[0], request(), *headers);
ASSERT_TRUE(result.has_value());
std::string expected_base =
@@ -898,7 +900,7 @@
ASSERT_EQ(1u, parsed->signatures.size());
EXPECT_EQ(0u, parsed->issues.size());
- std::optional<std::string> result =
+ base::expected<std::string, mojom::SRIMessageSignatureError> result =
ConstructSignatureBase(parsed->signatures[0], request(), *headers);
ASSERT_TRUE(result.has_value());
std::string expected_base =
@@ -946,7 +948,7 @@
EXPECT_EQ(0u, parsed->issues.size());
request_ = CreateRequest(*context_, test.url);
- std::optional<std::string> result =
+ base::expected<std::string, mojom::SRIMessageSignatureError> result =
ConstructSignatureBase(parsed->signatures[0], request(), *headers);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(expected_base.str(), result.value());
@@ -987,7 +989,7 @@
EXPECT_EQ(0u, parsed->issues.size());
request_ = CreateRequest(*context_, test.url);
- std::optional<std::string> result =
+ base::expected<std::string, mojom::SRIMessageSignatureError> result =
ConstructSignatureBase(parsed->signatures[0], request(), *headers);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(expected_base.str(), result.value());
@@ -1038,7 +1040,7 @@
EXPECT_EQ(0u, parsed->issues.size());
request_ = CreateRequest(*context_, test.url);
- std::optional<std::string> result =
+ base::expected<std::string, mojom::SRIMessageSignatureError> result =
ConstructSignatureBase(parsed->signatures[0], request(), *headers);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(expected_base.str(), result.value())
@@ -1068,7 +1070,7 @@
EXPECT_EQ(0u, parsed->issues.size());
request_ = CreateRequest(*context_, test.url);
- std::optional<std::string> result =
+ base::expected<std::string, mojom::SRIMessageSignatureError> result =
ConstructSignatureBase(parsed->signatures[0], request(), *headers);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(expected_base.str(), result.value())
@@ -1101,7 +1103,7 @@
EXPECT_EQ(0u, parsed->issues.size());
request_->set_method(test_method);
- std::optional<std::string> result =
+ base::expected<std::string, mojom::SRIMessageSignatureError> result =
ConstructSignatureBase(parsed->signatures[0], request(), *headers);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(expected_base.str(), result.value());
@@ -1148,7 +1150,7 @@
EXPECT_EQ(0u, parsed->issues.size());
request_ = CreateRequest(*context_, test.url);
- std::optional<std::string> result =
+ base::expected<std::string, mojom::SRIMessageSignatureError> result =
ConstructSignatureBase(parsed->signatures[0], request(), *headers);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(expected_base.str(), result.value());
@@ -1195,7 +1197,7 @@
EXPECT_EQ(0u, parsed->issues.size());
request_ = CreateRequest(*context_, test.url);
- std::optional<std::string> result =
+ base::expected<std::string, mojom::SRIMessageSignatureError> result =
ConstructSignatureBase(parsed->signatures[0], request(), *headers);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(expected_base.str(), result.value());
@@ -1234,7 +1236,7 @@
EXPECT_EQ(0u, parsed->issues.size());
request_ = CreateRequest(*context_, test.url);
- std::optional<std::string> result =
+ base::expected<std::string, mojom::SRIMessageSignatureError> result =
ConstructSignatureBase(parsed->signatures[0], request(), *headers);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(expected_base.str(), result.value());
@@ -1270,7 +1272,7 @@
ASSERT_EQ(1u, parsed->signatures.size());
EXPECT_EQ(0u, parsed->issues.size());
- std::optional<std::string> result =
+ base::expected<std::string, mojom::SRIMessageSignatureError> result =
ConstructSignatureBase(parsed->signatures[0], request(), *headers);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(expected_base.str(), result.value());
@@ -1329,7 +1331,7 @@
ASSERT_EQ(1u, parsed->signatures.size());
EXPECT_EQ(0u, parsed->issues.size());
- std::optional<std::string> result =
+ base::expected<std::string, mojom::SRIMessageSignatureError> result =
ConstructSignatureBase(parsed->signatures[0], request(), *headers);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(expected_base.str(), result.value());
@@ -1362,9 +1364,10 @@
ASSERT_EQ(1u, parsed->signatures.size());
EXPECT_EQ(0u, parsed->issues.size());
- std::optional<std::string> result =
+ base::expected<std::string, mojom::SRIMessageSignatureError> result =
ConstructSignatureBase(parsed->signatures[0], request(), *headers);
- EXPECT_THAT(result, testing::Optional(expected_base.str()));
+ ASSERT_TRUE(result.has_value());
+ EXPECT_EQ(expected_base.str(), result.value());
} while (std::next_permutation(params.begin(), params.end()));
}
@@ -1388,7 +1391,7 @@
ASSERT_EQ(1u, parsed->signatures.size());
EXPECT_EQ(0u, parsed->issues.size());
- std::optional<std::string> result =
+ base::expected<std::string, mojom::SRIMessageSignatureError> result =
ConstructSignatureBase(parsed->signatures[0], request(), *headers);
ASSERT_TRUE(result.has_value());
std::string expected_base =
@@ -1427,9 +1430,11 @@
ASSERT_EQ(1u, parsed->signatures.size());
EXPECT_EQ(0u, parsed->issues.size());
- std::optional<std::string> result =
+ base::expected<std::string, mojom::SRIMessageSignatureError> result =
ConstructSignatureBase(parsed->signatures[0], request(), *headers);
EXPECT_FALSE(result.has_value());
+ EXPECT_EQ(mojom::SRIMessageSignatureError::kSignatureBaseMissingHeader,
+ result.error());
}
// Then, add the header and verify success:
@@ -1441,7 +1446,7 @@
ASSERT_EQ(1u, parsed->signatures.size());
EXPECT_EQ(0u, parsed->issues.size());
- std::optional<std::string> result =
+ base::expected<std::string, mojom::SRIMessageSignatureError> result =
ConstructSignatureBase(parsed->signatures[0], request(), *headers);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(expected_base.str(), result.value());
@@ -1473,7 +1478,7 @@
ASSERT_EQ(1u, parsed->signatures.size());
EXPECT_EQ(0u, parsed->issues.size());
- std::optional<std::string> result =
+ base::expected<std::string, mojom::SRIMessageSignatureError> result =
ConstructSignatureBase(parsed->signatures[0], request(), *headers);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(expected_base.str(), result.value());
@@ -1654,9 +1659,12 @@
EXPECT_FALSE(
ValidateSRIMessageSignaturesOverHeaders(parsed, request(), *headers));
EXPECT_EQ(1u, parsed->issues.size());
- EXPECT_EQ(
- mojom::SRIMessageSignatureError::kValidationFailedSignatureMismatch,
- parsed->issues[0]->error);
+ mojom::SRIMessageSignatureError expected_error =
+ (strlen(test) == 0)
+ ? mojom::SRIMessageSignatureError::kSignatureBaseMissingHeader
+ : mojom::SRIMessageSignatureError::
+ kValidationFailedSignatureMismatch;
+ EXPECT_EQ(expected_error, parsed->issues[0]->error);
}
}
@@ -1680,10 +1688,29 @@
EXPECT_FALSE(
ValidateSRIMessageSignaturesOverHeaders(parsed, request(), *headers));
ASSERT_EQ(1u, parsed->issues.size());
- EXPECT_EQ(mojom::SRIMessageSignatureError::kValidationFailedSignatureMismatch,
+ EXPECT_EQ(mojom::SRIMessageSignatureError::kSignatureBaseMissingHeader,
parsed->issues[0]->error);
}
+TEST_F(SRIMessageSignatureValidationTest, InvalidUnencodedDigest) {
+ // `unencoded-digest` must be a valid structured field dictionary. If it's
+ // not, signature base generation should fail.
+ scoped_refptr<net::HttpResponseHeaders> headers = Headers(
+ "not a dictionary", kValidSignatureHeader, kValidSignatureInputHeader);
+
+ mojom::SRIMessageSignaturesPtr parsed =
+ ParseSRIMessageSignaturesFromHeaders(*headers);
+ ASSERT_EQ(1u, parsed->signatures.size());
+ EXPECT_EQ(0u, parsed->issues.size());
+
+ EXPECT_FALSE(
+ ValidateSRIMessageSignaturesOverHeaders(parsed, request(), *headers));
+ ASSERT_EQ(1u, parsed->issues.size());
+ EXPECT_EQ(
+ mojom::SRIMessageSignatureError::kSignatureBaseInvalidUnencodedDigest,
+ parsed->issues[0]->error);
+}
+
class SRIMessageSignatureEnforcementTest
: public SRIMessageSignatureValidationTest {
protected:
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/issues/sri-message-signatures/signature-base-missing-header-expected.txt b/third_party/blink/web_tests/http/tests/inspector-protocol/issues/sri-message-signatures/signature-base-missing-header-expected.txt
new file mode 100644
index 0000000..7dd7119
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/issues/sri-message-signatures/signature-base-missing-header-expected.txt
@@ -0,0 +1,19 @@
+Verifies issue creation for a missing header in the signature base.
+Issue reported: {
+ issue : {
+ code : SRIMessageSignatureIssue
+ details : {
+ sriMessageSignatureIssueDetails : {
+ error : SignatureBaseMissingHeader
+ integrityAssertions : [
+ ]
+ request : {
+ requestId : <string>
+ url : http://127.0.0.1:8000/inspector-protocol/resources/sri-message-signature-test.php?input=signature%3D%28%22unencoded-digest%22%3Bsf+%22x-test-header%22%29%3Bkeyid%3D%22JrQLj5P%2F89iXES9%2BvFgrIy29clF9CC%2FoPPsw3c5D0bs%3D%22%3Btag%3D%22sri%22&signature=signature%3D%3ASbCdPUyjc0IBJjFbVRWs81ucEUcFz87b37nQ63d6kDW%2B%2FJvDmET6O5cSdwlddePvlwemLdaWFuY6pQGO%2BhrkAg%3D%3D%3A&digest=sha-256%3D%3AX48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE%3D%3A
+ }
+ signatureBase :
+ }
+ }
+ }
+}
+
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/issues/sri-message-signatures/signature-base-missing-header.js b/third_party/blink/web_tests/http/tests/inspector-protocol/issues/sri-message-signatures/signature-base-missing-header.js
new file mode 100644
index 0000000..26ff236
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/issues/sri-message-signatures/signature-base-missing-header.js
@@ -0,0 +1,30 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+(async function(/** @type {import('test_runner').TestRunner} */ testRunner) {
+ const {page, session, dp} = await testRunner.startBlank("Verifies issue creation for a missing header in the signature base.");
+
+ await dp.Network.enable();
+ await dp.Audits.enable();
+ const url = 'inspector-protocol/network/resources/hello-world.html';
+ await page.navigate(url);
+ const issuePromise = dp.Audits.onceIssueAdded(e => {
+ return e.params.issue.code === 'SRIMessageSignatureIssue';
+ });
+
+ const signature_input = `signature=("unencoded-digest";sf "x-test-header");keyid="JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=";tag="sri"`;
+ const signature = `signature=:SbCdPUyjc0IBJjFbVRWs81ucEUcFz87b37nQ63d6kDW+/JvDmET6O5cSdwlddePvlwemLdaWFuY6pQGO+hrkAg==:`;
+ const digest = `sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:`;
+
+ let testURL = new URL('/inspector-protocol/resources/sri-message-signature-test.php', self.origin);
+ testURL.searchParams.set('input', signature_input);
+ testURL.searchParams.set('signature', signature);
+ testURL.searchParams.set('digest', digest);
+ await session.evaluate(`fetch('${testURL.href}')`);
+
+ // Dump the issue:
+ const issue = await issuePromise;
+ testRunner.log(issue.params, "Issue reported: ");
+ testRunner.completeTest();
+})
Original Bug Report
SRI Message Signature bypass via empty signature base fallback
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the security team.
Overview: A logic error in the Network Service’s SRI Message Signature validation allows an attacker to bypass subresource integrity checks. If a required HTTP header like Unencoded-Digest is missing, the signature base construction fails but erroneously falls back to an empty string instead of aborting. An attacker can exploit this by serving a malicious script without an Unencoded-Digest header and providing a valid Ed25519 signature for the empty string.
Affected files:
services/network/public/cpp/sri_message_signatures.ccthird_party/blink/renderer/platform/loader/subresource_integrity.cc
Estimated timestamp from git blame: 2025-07-31
Description
A potential logic error exists in services/network/public/cpp/sri_message_signatures.cc that allows an attacker to bypass Subresource Integrity (SRI) Message Signatures.
When ValidateSRIMessageSignaturesOverHeaders attempts to verify a signature, it calls ConstructSignatureBase to generate the message that was signed. According to RFC 9421, if a required component (such as the mandatory unencoded-digest or another requested HTTP header) is missing from the response, signature base construction must fail. Indeed, ConstructSignatureBase correctly returns std::nullopt in this scenario (line 731).
However, ValidateSRIMessageSignaturesOverHeaders incorrectly handles this return value at lines 806-808:
std::string signature_base =
ConstructSignatureBase(message_signature, url_request, headers)
.value_or("");
By using .value_or(""), a failure in signature base construction results in the verification proceeding with a 0-byte (empty) string. ED25519_verify is then called against this empty string.
Simultaneously, in Blink (third_party/blink/renderer/platform/loader/subresource_integrity.cc:174-200), if the Unencoded-Digest HTTP header is completely omitted from the response, CheckUnencodedDigests trivially returns true, skipping the validation of the response body. Blink’s subsequent signature check (CheckSignaturesImpl) relies on the Network Service having already cryptographically validated the signature, only verifying that the keyid matches the expected public key.
This coupling allows an attacker to completely decouple the signature from the payload, bypassing SRI if they can obtain a signature for the empty string.
Potential Steps to Reproduce
Note: These are suggested/potential steps derived from code analysis; our tooling agent does not have the ability to run code to produce a live proof-of-concept.
- A victim visits a page with a script protected by signature-based SRI:
<script src="..." integrity="ed25519-<PUBKEY>">. - An attacker intercepts the request and serves a malicious script payload (e.g.,
alert(1)). - The attacker obtains a valid Ed25519 signature for the empty string
""under<PUBKEY>(e.g., via cross-protocol key reuse, an endpoint that signs arbitrary input, or a signing oracle). - The attacker crafts the HTTP response with the following headers:
Signature-Input: sig1=("unencoded-digest");keyid="<PUBKEY>";tag="ed25519-integrity"Signature: sig1=:<SIG_OF_EMPTY_STRING>:
- Crucially, the attacker omits the
Unencoded-DigestHTTP header entirely. - The Network Service attempts to construct the signature base, fails because
Unencoded-Digestis missing, defaults to"", and successfully verifies the attacker’s empty-string signature. The response is permitted. - Blink receives the response. Because
Unencoded-Digestis missing,CheckUnencodedDigestssucceeds.CheckSignaturesImplfinds thekeyidmatches and allows the malicious script to execute.
Suggested Fix
In services/network/public/cpp/sri_message_signatures.cc, do not use .value_or("") when constructing the signature base. Instead, fail the validation if ConstructSignatureBase returns std::nullopt:
std::optional<std::string> signature_base_opt =
ConstructSignatureBase(message_signature, url_request, headers);
if (!signature_base_opt.has_value()) {
AddIssueFromErrorEnum(
mojom::SRIMessageSignatureError::kValidationFailedSignatureMismatch, // Or a new, more specific error enum
message_signatures->issues);
return false;
}
std::string signature_base = signature_base_opt.value();
Additionally, consider hardening Blink’s Resource::CheckResourceIntegrity to explicitly fail if signature-based SRI is required but the Unencoded-Digest header is missing.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.