CVE-2025-6556
Overview
Background
- Mixed content autoupgrade
- Chrome’s behavior of transparently rewriting insecure (
http:) subresource requests tohttps:when the page is served over a secure/mixed-content-restricting context, controlled by thekMixedContentAutoupgradefeature. - Opaque origin and precursor
- A
data:(or sandboxed) document has an opaqueSecurityOriginwhoseGetSecurityOrigin()is effectively null, but it records a precursor origin retrievable viaGetOriginOrPrecursorOriginIfOpaque(). - `HttpsState`
- A summary of a context’s transport security (
kNone,kModern, …) derived from the fetch client settings object’s security origin. - `ShouldTreatURLSchemeAsRestrictingMixedContent`
- A
SchemeRegistrypredicate that reports whether a scheme (e.g.,https) causes its context to restrict mixed content.
Root Cause Analysis
ShouldAutoupgrade decided whether to autoupgrade by reading fetch_client_settings_object->GetHttpsState() and requiring it to be kModern, whereas the parallel IsMixedContent restriction check inspected the origin or its precursor if opaque. For an opaque origin (such as a data: document) whose precursor is https, the security origin used to compute HttpsState is null, so https_state == HttpsState::kNone and the old condition https_state == HttpsState::kNone bailed out early — leaving the insecure subresource not autoupgraded even though mixed content would still be restricted in that context.
The fix threads a LocalFrame* through UpgradeInsecureRequest and ShouldAutoupgrade and, when a frame is available, replaces the HttpsState test with the new IsMixedContentRestrictedInFrameContext, which checks both the top frame and the frame’s own security context using GetOriginOrPrecursorOriginIfOpaque()->Protocol() against ShouldTreatURLSchemeAsRestrictingMixedContent. This makes the autoupgrade decision use exactly the same opaque-origin-aware criterion as the mixed-content restriction check, so requests that should be upgraded now are; when no frame is present the code falls back to the prior HttpsState::kModern behavior. The change tightens enforcement (more requests get upgraded), correcting an under-enforcement, not a bypass of an over-broad rule.
ShouldAutoupgrade and IsMixedContent used inconsistent security-origin logic — the former ignored the precursor of opaque origins while the latter honored it — so mixed content in an opaque-but-secure-precursor context escaped autoupgrade; the fix routes both through the same precursor-aware, frame-based restriction check.Attack Path
- Serve a secure page that spawns an opaque context
A page on
https://creates adata:document (opaque origin) whose precursor origin is the securehttpsorigin, matching the regression test’sdata:text/html,...with precursorhttps://example.test. - Request an insecure subresource
The opaque document loads an
http:subresource (e.g., an image), which is mixed content that policy intends to autoupgrade. - Autoupgrade skipped
Because
GetHttpsState()isHttpsState::kNonefor the opaque origin, the pre-patchShouldAutoupgradereturns early and the request is left as insecurehttp:(IsAutomaticUpgrade()false), so it goes out in cleartext instead of being upgraded. - Network exposure The unencrypted request is exposed to a network attacker, enabling interception or tampering of the subresource; this is a policy under-enforcement, and any further compromise is (inferred).
Impact Assessment
http: content, and any escalation beyond cleartext exposure is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/loader/mixed_content_checker.cc |
modified |
Files Changed
third_party/blink/renderer/core/frame/remote_frame.ccthird_party/blink/renderer/core/loader/frame_loader.ccthird_party/blink/renderer/core/loader/mixed_content_checker.ccthird_party/blink/renderer/core/loader/mixed_content_checker.hthird_party/blink/renderer/core/loader/mixed_content_checker_test.cc
Audit Directions
- Divergent origin handling for the same decisionLook for pairs of security checks (restrict vs. upgrade, block vs. warn) that should agree but read the origin differently, especially where one ignores
GetOriginOrPrecursorOriginIfOpaque(). - Opaque-origin edge casesAudit loader/security code that keys decisions on
GetSecurityOrigin()/GetHttpsState()without accounting for opaque origins with a secure precursor (data:, sandboxed iframes,blob:). - Feature-gated early returnsReview
ShouldAutoupgrade-style predicates whose earlyreturns can silently disable an enforcement path, and confirm the disabling condition matches the intended policy scope.
Patch
From 3e9801630f7a3697936eb90d31185542abd8f60c Mon Sep 17 00:00:00 2001
From: Carlos IL <carlosil@chromium.org>
Date: Wed, 07 May 2025 18:32:19 -0700
Subject: [PATCH] Mixed Content: Use the same check for ShouldAutoUpgrade and IsMixedContent
Currently IsMixedContent determines whether mixed content is restricted
in a particular context by checking if its security origin or precursor
if opaque is https (https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/loader/mixed_content_checker.cc;drc=8d201f296ea4efda4529e69fd9509be8abd63156;l=293), whereas ShouldAutoupgrade only checks the
security origin, which is null if opaque (https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/loader/mixed_content_checker.cc;drc=8d201f296ea4efda4529e69fd9509be8abd63156;l=867).
This can lead to some requests not being autoupgraded when they should be.
Bug: 40062462
Change-Id: I10c381e407a0693ae262533027fad9e2c37fa365
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6388538
Auto-Submit: Carlos IL <carlosil@chromium.org>
Commit-Queue: Carlos IL <carlosil@chromium.org>
Reviewed-by: Takashi Toyoshima <toyoshim@chromium.org>
Reviewed-by: Emily Stark <estark@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1457364}
---
diff --git a/third_party/blink/renderer/core/frame/remote_frame.cc b/third_party/blink/renderer/core/frame/remote_frame.cc
index b5b166c..96b0b13d 100644
--- a/third_party/blink/renderer/core/frame/remote_frame.cc
+++ b/third_party/blink/renderer/core/frame/remote_frame.cc
@@ -212,7 +212,8 @@
frame_request.GetResourceRequest(), fetch_client_settings_object, window,
frame_request.GetFrameType(),
window->GetFrame() ? window->GetFrame()->GetContentSettingsClient()
- : nullptr);
+ : nullptr,
+ window->GetFrame());
if (NavigationShouldReplaceCurrentHistoryEntry(frame_load_type))
frame_load_type = WebFrameLoadType::kReplaceCurrentItem;
diff --git a/third_party/blink/renderer/core/loader/frame_loader.cc b/third_party/blink/renderer/core/loader/frame_loader.cc
index bb0848d..eb4422f9 100644
--- a/third_party/blink/renderer/core/loader/frame_loader.cc
+++ b/third_party/blink/renderer/core/loader/frame_loader.cc
@@ -1797,7 +1797,7 @@
MixedContentChecker::UpgradeInsecureRequest(
resource_request, fetch_client_settings_object, window_for_logging,
- frame_type, frame_->GetContentSettingsClient());
+ frame_type, frame_->GetContentSettingsClient(), frame_);
}
void FrameLoader::WriteIntoTrace(perfetto::TracedValue context) const {
diff --git a/third_party/blink/renderer/core/loader/mixed_content_checker.cc b/third_party/blink/renderer/core/loader/mixed_content_checker.cc
index c3a04f6..92fbcfcef6 100644
--- a/third_party/blink/renderer/core/loader/mixed_content_checker.cc
+++ b/third_party/blink/renderer/core/loader/mixed_content_checker.cc
@@ -318,6 +318,31 @@
}
// static
+bool MixedContentChecker::IsMixedContentRestrictedInFrameContext(
+ LocalFrame* frame) {
+ if (!frame) {
+ return false;
+ }
+ // Check the top frame first.
+ Frame& top = frame->Tree().Top();
+ if (SchemeRegistry::ShouldTreatURLSchemeAsRestrictingMixedContent(
+ top.GetSecurityContext()
+ ->GetSecurityOrigin()
+ ->GetOriginOrPrecursorOriginIfOpaque()
+ ->Protocol())) {
+ return true;
+ }
+ if (SchemeRegistry::ShouldTreatURLSchemeAsRestrictingMixedContent(
+ frame->GetSecurityContext()
+ ->GetSecurityOrigin()
+ ->GetOriginOrPrecursorOriginIfOpaque()
+ ->Protocol())) {
+ return true;
+ }
+ return false;
+}
+
+// static
Frame* MixedContentChecker::InWhichFrameIsContentMixed(LocalFrame* frame,
const KURL& url) {
// Frameless requests cannot be mixed content.
@@ -858,14 +883,22 @@
mojom::blink::RequestContextType type,
WebContentSettingsClient* settings_client,
const ResourceRequest& resource_request,
- ExecutionContext* execution_context_for_logging) {
- const HttpsState https_state = fetch_client_settings_object->GetHttpsState();
+ ExecutionContext* execution_context_for_logging,
+ LocalFrame* frame) {
const KURL& request_url = resource_request.Url();
// We are currently not autoupgrading plugin loaded content, which is why
// check_mode_for_plugin is hardcoded to kStrict.
+ bool settings_restricts_mixed_content;
+ if (frame) {
+ settings_restricts_mixed_content =
+ IsMixedContentRestrictedInFrameContext(frame);
+ } else {
+ settings_restricts_mixed_content =
+ fetch_client_settings_object->GetHttpsState() == HttpsState::kModern;
+ }
if (!base::FeatureList::IsEnabled(
blink::features::kMixedContentAutoupgrade) ||
- https_state == HttpsState::kNone ||
+ !settings_restricts_mixed_content ||
MixedContent::ContextTypeFromRequestContext(
type, MixedContent::CheckModeForPlugin::kStrict) !=
mojom::blink::MixedContentContextType::kOptionallyBlockable) {
@@ -1000,7 +1033,8 @@
const FetchClientSettingsObject* fetch_client_settings_object,
ExecutionContext* execution_context_for_logging,
mojom::RequestContextFrameType frame_type,
- WebContentSettingsClient* settings_client) {
+ WebContentSettingsClient* settings_client,
+ LocalFrame* frame) {
// We always upgrade requests that meet any of the following criteria:
// 1. Are for subresources.
// 2. Are for nested frames.
@@ -1025,7 +1059,7 @@
if (context == mojom::blink::RequestContextType::UNSPECIFIED ||
!MixedContentChecker::ShouldAutoupgrade(
fetch_client_settings_object, context, settings_client,
- resource_request, execution_context_for_logging)) {
+ resource_request, execution_context_for_logging, frame)) {
return;
}
// We set the upgrade if insecure flag regardless of whether we autoupgrade
diff --git a/third_party/blink/renderer/core/loader/mixed_content_checker.h b/third_party/blink/renderer/core/loader/mixed_content_checker.h
index 1c8149eb1..39e3c66 100644
--- a/third_party/blink/renderer/core/loader/mixed_content_checker.h
+++ b/third_party/blink/renderer/core/loader/mixed_content_checker.h
@@ -112,7 +112,8 @@
mojom::blink::RequestContextType type,
WebContentSettingsClient* settings_client,
const ResourceRequest& resource_request,
- ExecutionContext* execution_context_for_logging);
+ ExecutionContext* execution_context_for_logging,
+ LocalFrame* frame);
static mojom::blink::MixedContentContextType ContextTypeForInspector(
LocalFrame*,
@@ -152,7 +153,8 @@
const FetchClientSettingsObject* fetch_client_settings_object,
ExecutionContext* execution_context_for_logging,
mojom::RequestContextFrameType,
- WebContentSettingsClient* settings_client);
+ WebContentSettingsClient* settings_client,
+ LocalFrame* frame);
static MixedContent::CheckModeForPlugin DecideCheckModeForPlugin(Settings*);
@@ -162,6 +164,8 @@
private:
FRIEND_TEST_ALL_PREFIXES(MixedContentCheckerTest, HandleCertificateError);
+ static bool IsMixedContentRestrictedInFrameContext(LocalFrame* frame);
+
static Frame* InWhichFrameIsContentMixed(LocalFrame*, const KURL&);
static ConsoleMessage* CreateConsoleMessageAboutFetch(
diff --git a/third_party/blink/renderer/core/loader/mixed_content_checker_test.cc b/third_party/blink/renderer/core/loader/mixed_content_checker_test.cc
index 56a4545e..c95b504 100644
--- a/third_party/blink/renderer/core/loader/mixed_content_checker_test.cc
+++ b/third_party/blink/renderer/core/loader/mixed_content_checker_test.cc
@@ -16,6 +16,8 @@
#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-blink.h"
#include "third_party/blink/public/mojom/loader/mixed_content.mojom-blink.h"
#include "third_party/blink/public/mojom/loader/request_context_frame_type.mojom-blink.h"
+#include "third_party/blink/renderer/core/execution_context/security_context.h"
+#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/loader/empty_clients.h"
@@ -349,7 +351,9 @@
// These are not used in test, but need to be implemented since they are pure
// virtual.
const KURL& BaseUrl() const override { return url; }
- const SecurityOrigin* GetSecurityOrigin() const override { return nullptr; }
+ const SecurityOrigin* GetSecurityOrigin() const override {
+ return origin_.get();
+ }
network::mojom::ReferrerPolicy GetReferrerPolicy() const override {
return network::mojom::ReferrerPolicy::kAlways;
}
@@ -362,10 +366,19 @@
const override {
return set;
}
+ void SetSecurityOrigin(String origin_url, String reference_origin) {
+ KURL origin_kurl(origin_url);
+ scoped_refptr<SecurityOrigin> reference =
+ SecurityOrigin::CreateFromString(reference_origin);
+ origin_ = SecurityOrigin::CreateWithReferenceOrigin(KURL(origin_url),
+ reference.get());
+ }
+ scoped_refptr<SecurityOrigin> GetSecurityOrigin() { return origin_; }
Regression Test / PoC
diff --git a/third_party/blink/renderer/core/loader/mixed_content_checker_test.cc b/third_party/blink/renderer/core/loader/mixed_content_checker_test.cc
index 56a4545e..c95b504 100644
--- a/third_party/blink/renderer/core/loader/mixed_content_checker_test.cc
+++ b/third_party/blink/renderer/core/loader/mixed_content_checker_test.cc
@@ -16,6 +16,8 @@
#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-blink.h"
#include "third_party/blink/public/mojom/loader/mixed_content.mojom-blink.h"
#include "third_party/blink/public/mojom/loader/request_context_frame_type.mojom-blink.h"
+#include "third_party/blink/renderer/core/execution_context/security_context.h"
+#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/loader/empty_clients.h"
@@ -349,7 +351,9 @@
// These are not used in test, but need to be implemented since they are pure
// virtual.
const KURL& BaseUrl() const override { return url; }
- const SecurityOrigin* GetSecurityOrigin() const override { return nullptr; }
+ const SecurityOrigin* GetSecurityOrigin() const override {
+ return origin_.get();
+ }
network::mojom::ReferrerPolicy GetReferrerPolicy() const override {
return network::mojom::ReferrerPolicy::kAlways;
}
@@ -362,10 +366,19 @@
const override {
return set;
}
+ void SetSecurityOrigin(String origin_url, String reference_origin) {
+ KURL origin_kurl(origin_url);
+ scoped_refptr<SecurityOrigin> reference =
+ SecurityOrigin::CreateFromString(reference_origin);
+ origin_ = SecurityOrigin::CreateWithReferenceOrigin(KURL(origin_url),
+ reference.get());
+ }
+ scoped_refptr<SecurityOrigin> GetSecurityOrigin() { return origin_; }
private:
const KURL url = KURL("https://example.test");
const InsecureNavigationsSet set;
+ scoped_refptr<SecurityOrigin> origin_;
};
TEST(MixedContentCheckerTest,
@@ -376,12 +389,17 @@
request.SetRequestContext(mojom::blink::RequestContextType::AUDIO);
TestFetchClientSettingsObject* settings =
MakeGarbageCollected<TestFetchClientSettingsObject>();
+ settings->SetSecurityOrigin("https://example.test", "");
// Used to get a non-null document.
DummyPageHolder holder;
+ holder.GetFrame()
+ .DomWindow()
+ ->GetSecurityContext()
+ .SetSecurityOriginForTesting(settings->GetSecurityOrigin());
MixedContentChecker::UpgradeInsecureRequest(
request, settings, holder.GetDocument().GetExecutionContext(),
- mojom::RequestContextFrameType::kTopLevel, nullptr);
+ mojom::RequestContextFrameType::kTopLevel, nullptr, &holder.GetFrame());
EXPECT_FALSE(request.IsAutomaticUpgrade());
EXPECT_TRUE(request.UpgradeIfInsecure());
@@ -394,12 +412,18 @@
request.SetRequestContext(mojom::blink::RequestContextType::AUDIO);
TestFetchClientSettingsObject* settings =
MakeGarbageCollected<TestFetchClientSettingsObject>();
+ settings->SetSecurityOrigin("https://example.test", "");
+
// Used to get a non-null document.
DummyPageHolder holder;
+ holder.GetFrame()
+ .DomWindow()
+ ->GetSecurityContext()
+ .SetSecurityOriginForTesting(settings->GetSecurityOrigin());
MixedContentChecker::UpgradeInsecureRequest(
request, settings, holder.GetDocument().GetExecutionContext(),
- mojom::RequestContextFrameType::kTopLevel, nullptr);
+ mojom::RequestContextFrameType::kTopLevel, nullptr, &holder.GetFrame());
EXPECT_TRUE(request.IsAutomaticUpgrade());
EXPECT_TRUE(request.UpgradeIfInsecure());
@@ -413,12 +437,18 @@
request.SetRequestContext(mojom::blink::RequestContextType::AUDIO);
TestFetchClientSettingsObject* settings =
MakeGarbageCollected<TestFetchClientSettingsObject>();
+ settings->SetSecurityOrigin("https://example.test", "");
+
// Used to get a non-null document.
DummyPageHolder holder;
+ holder.GetFrame()
+ .DomWindow()
+ ->GetSecurityContext()
+ .SetSecurityOriginForTesting(settings->GetSecurityOrigin());
MixedContentChecker::UpgradeInsecureRequest(
request, settings, holder.GetDocument().GetExecutionContext(),
- mojom::RequestContextFrameType::kTopLevel, nullptr);
+ mojom::RequestContextFrameType::kTopLevel, nullptr, &holder.GetFrame());
EXPECT_FALSE(request.IsAutomaticUpgrade());
EXPECT_FALSE(request.UpgradeIfInsecure());
@@ -432,15 +462,76 @@
request.SetRequestContext(mojom::blink::RequestContextType::AUDIO);
TestFetchClientSettingsObject* settings =
MakeGarbageCollected<TestFetchClientSettingsObject>();
+ settings->SetSecurityOrigin("https://example.test", "");
+
// Used to get a non-null document.
DummyPageHolder holder;
+ holder.GetFrame()
+ .DomWindow()
+ ->GetSecurityContext()
+ .SetSecurityOriginForTesting(settings->GetSecurityOrigin());
MixedContentChecker::UpgradeInsecureRequest(
request, settings, holder.GetDocument().GetExecutionContext(),
- mojom::RequestContextFrameType::kTopLevel, nullptr);
+ mojom::RequestContextFrameType::kTopLevel, nullptr, &holder.GetFrame());
EXPECT_FALSE(request.IsAutomaticUpgrade());
EXPECT_FALSE(request.UpgradeIfInsecure());
}
+TEST(MixedContentCheckerTest,
+ AutoupgradeMixedContentInOpaqueOriginIfPrecursorIsSecure) {
+ test::TaskEnvironment task_environment;
+ ResourceRequest request;
+ request.SetUrl(KURL("http://example.test"));
+ request.SetRequestContext(mojom::blink::RequestContextType::IMAGE);
+ TestFetchClientSettingsObject* settings =
+ MakeGarbageCollected<TestFetchClientSettingsObject>();
+ // Set the security origin to an opaque one, with a secure precursor.
+ settings->SetSecurityOrigin(
+ "data:text/html,<img src=http://example.test/insecureimage.jpg>",
+ "https://example.test");
+
+ // Used to get a non-null document.
+ DummyPageHolder holder;
+ holder.GetFrame()
+ .DomWindow()
+ ->GetSecurityContext()
+ .SetSecurityOriginForTesting(settings->GetSecurityOrigin());
+
+ MixedContentChecker::UpgradeInsecureRequest(
+ request, settings, holder.GetDocument().GetExecutionContext(),
+ mojom::RequestContextFrameType::kTopLevel, nullptr, &holder.GetFrame());
+
+ EXPECT_TRUE(request.IsAutomaticUpgrade());
+ EXPECT_TRUE(request.UpgradeIfInsecure());
+}
+
+TEST(MixedContentCheckerTest,
+ DontAutoupgradeMixedContentInOpaqueOriginIfPrecursorIsNotSecure) {
+ test::TaskEnvironment task_environment;
+ ResourceRequest request;
+ request.SetUrl(KURL("http://example.test"));
+ request.SetRequestContext(mojom::blink::RequestContextType::IMAGE);
+ TestFetchClientSettingsObject* settings =
+ MakeGarbageCollected<TestFetchClientSettingsObject>();
+ // Set the security origin to an opaque one, with a not secure precursor.
+ settings->SetSecurityOrigin(
+ "data:text/html,<img src=http://example.test/insecureimage.jpg>",
+ "http://example.test");
+
+ // Used to get a non-null document.
+ DummyPageHolder holder;
+ holder.GetFrame()
+ .DomWindow()
+ ->GetSecurityContext()
+ .SetSecurityOriginForTesting(settings->GetSecurityOrigin());
+
+ MixedContentChecker::UpgradeInsecureRequest(
+ request, settings, holder.GetDocument().GetExecutionContext(),
+ mojom::RequestContextFrameType::kTopLevel, nullptr, &holder.GetFrame());
+
+ EXPECT_FALSE(request.IsAutomaticUpgrade());
+}
+
} // namespace blink
Original Bug Report
Security: Possible to include mixed content in an about:blank popup opened by a https page
VULNERABILITY DETAILS
Typically, a https site can’t use or access resources from a http site. The browser will block attempts by a https site to include a http resources.
this vulnerability is similar to https://bugs.chromium.org/p/chromium/issues/detail?id=957002
REPRODUCTION CASE
-
Load a https site example: https://www.google.com
-
Use console to open popup tab using:
w = open(“about:blank”);
- Try to load http hosted image using:
w.document.write(’<img src=“http://www.clascertification.com/_img/_public/actualites/medium_52-Cap_Sici.jpg">');
in the above case the system will warn about mixed content and upgrades the link to https (http://www.clascertification.com/_img/_public/actualites/medium_52-Cap_Sici.jpg).
- Try to load again using data uri inside iframe using:
w.document.write(’<iframe src=“data:text/html,<img src=http://www.clascertification.com/_img/_public/actualites/medium_52-Cap_Sici.jpg>"></iframe>’);
Here the system also warns about mixed content but fails to upgrade the link to https.
In Case of Step 3:
Mixed Content: The page at ‘https://www.google.com/' was loaded over HTTPS, but requested an insecure element ‘http://www.clascertification.com/_img/_public/actualites/medium_52-Cap_Sici.jpg'. This request was automatically upgraded to HTTPS, For more information see https://blog.chromium.org/2019/10/no-more-mixed-messages-about-https.html
In Case of Step 4:
Mixed Content: The page at ‘https://www.google.com/' was loaded over HTTPS, but requested an insecure image ‘http://www.clascertification.com/_img/_public/actualites/medium_52-Cap_Sici.jpg'. This content should also be served over HTTPS.
First one upgrades while second one fails to upgrade but warns with (This content should also be served over HTTPS).