CVE-2026-87599
Overview
Background
- Insecure form warning
- Chrome’s
InsecureFormNavigationThrottleinterposes on form submissions and shows an interstitial when a secure (HTTPS) page posts a form to an insecure (HTTP)actionendpoint. - `IsInsecureFormActionOnSecureSource`
- The helper in
insecure_form_util.ccthat decides whether a submission qualifies as an insecure-form case by testing that the source origin is HTTPS and the action URL is not secure. - Opaque origin
- A
url::Originwith no tuple of its own (empty scheme/host/port), produced for sandboxed iframes,data:documents, and similar contexts; it may still carry a precursor tuple recording the origin it was derived from. - `GetTupleOrPrecursorTupleIfOpaque`
- A
url::Originaccessor that returns the origin’s own tuple when it is a normal origin, or the precursor tuple (e.g. the derivinghttps://…origin) when the origin is opaque.
Root Cause Analysis
IsInsecureFormActionOnSecureSource gated the entire insecure-form check on base::EqualsCaseInsensitiveASCII(source_origin.scheme(), url::kHttpsScheme). For an opaque origin, source_origin.scheme() returns an empty string rather than "https", so the negated comparison was true and the function took the early path and returned false — meaning an opaque-origin form (such as a sandboxed iframe derived from an HTTPS page) submitting to an http:// action was never classified as an insecure form. The violated invariant is that a document whose effective security context is HTTPS must have its cleartext form submissions throttled regardless of whether its origin is expressed as a tuple or as an opaque origin.
The fix replaces source_origin.scheme() with source_origin.GetTupleOrPrecursorTupleIfOpaque().scheme(), so opaque origins are judged by the scheme of their precursor tuple; an opaque origin derived from https:// now correctly compares equal to url::kHttpsScheme and its insecure action is throttled. This tightens the check: the added unit tests assert EXPECT_TRUE for an opaque HTTPS-derived source with an HTTP action, while still returning false for HTTPS actions, HTTP-derived opaque sources, and a bare url::Origin().
GetTupleOrPrecursorTupleIfOpaque() restores the intended HTTPS classification for those origins.Attack Path
- Host an opaque embedded form
An attacker or malicious content on an HTTPS page places the form inside an opaque-origin context (e.g. a sandboxed iframe derived from the HTTPS origin) whose
url::Originreports an empty scheme. - Point the action at HTTP
The form’s
actiontargets anhttp://(cleartext) endpoint, creating the mixed-content submission the throttle is meant to warn about. - Fail the HTTPS scheme test
IsInsecureFormActionOnSecureSourcecompares the opaque origin’s emptyscheme()tourl::kHttpsScheme, the comparison is unequal, and the function returnsfalse. - Bypass the throttle
Because the submission is not classified as an insecure form,
InsecureFormNavigationThrottlepresents no interstitial and the form data is sent over cleartext without user warning. - Interception of submitted data (inferred) A network attacker positioned on the path can read or tamper with the cleartext submission that would otherwise have been gated behind the warning.
Impact Assessment
action; exploitation of the resulting cleartext to intercept or modify submitted data requires a network man-in-the-middle position (inferred). The advisory classifies this as improper input validation in Interstitials and does not report exploitation in the wild.Files Changed
components/security_interstitials/core/insecure_form_util_unittest.cccomponents/security_interstitials/core/insecure_form_util.cc
Audit Directions
- Opaque-origin scheme checksSearch for security decisions that call
Origin::scheme()(or compare againsturl::kHttpsScheme/kHttpScheme) directly; opaque origins report an empty scheme, so these should usually useGetTupleOrPrecursorTupleIfOpaque()when the effective security context matters. - Sandboxed/derived contexts in mixed-content and throttle logicReview other navigation throttles and mixed-content guards for assumptions that a secure document always presents a tuple origin, which sandboxed iframes and
data:/blob:documents violate. - Precursor-tuple omissionsAudit origin-based allow/deny helpers where an opaque origin should inherit its precursor’s security properties but is instead treated as unknown, causing either an over-permissive fall-through (as here) or an over-restrictive block.
Patch
From d7a95acf2a5af92b6d94bff01a6897c01df4dbb1 Mon Sep 17 00:00:00 2001
From: Ari Chivukula <arichiv@chromium.org>
Date: Tue, 28 Jul 2026 18:08:17 -0700
Subject: [PATCH] Resolve InsecureFormNavigationThrottle bypass
Ensure proper throttling of opaque embedded forms.
Fixed: 513702096
Change-Id: I6f0298581b49a29ce8743419044fcfbd5ade4a98
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8164181
Auto-Submit: Ari Chivukula <arichiv@chromium.org>
Commit-Queue: Carlos IL <carlosil@chromium.org>
Reviewed-by: Carlos IL <carlosil@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1669917}
---
diff --git a/components/security_interstitials/core/insecure_form_util.cc b/components/security_interstitials/core/insecure_form_util.cc
index a7fe5564..4ae57fe 100644
--- a/components/security_interstitials/core/insecure_form_util.cc
+++ b/components/security_interstitials/core/insecure_form_util.cc
@@ -43,8 +43,10 @@
bool IsInsecureFormActionOnSecureSource(const url::Origin& source_origin,
const GURL& action_url) {
- if (!base::EqualsCaseInsensitiveASCII(source_origin.scheme(),
- url::kHttpsScheme)) {
+ // For opaque origins the precursos tuple must be examined.
+ if (!base::EqualsCaseInsensitiveASCII(
+ source_origin.GetTupleOrPrecursorTupleIfOpaque().scheme(),
+ url::kHttpsScheme)) {
#if BUILDFLAG(IS_IOS)
// On iOS, tests can't use an HTTPS server that serves a valid HTTPS
// response. Check if the URL is treated as secure for testing purposes.
diff --git a/components/security_interstitials/core/insecure_form_util_unittest.cc b/components/security_interstitials/core/insecure_form_util_unittest.cc
index f62a178..3223b18 100644
--- a/components/security_interstitials/core/insecure_form_util_unittest.cc
+++ b/components/security_interstitials/core/insecure_form_util_unittest.cc
@@ -54,4 +54,19 @@
url::Origin::Create(GURL("http://127.0.0.1:123")),
GURL("http://example.com")));
#endif
+
+ // Opaque https source with insecure action still counts.
+ EXPECT_TRUE(IsInsecureFormActionOnSecureSource(
+ url::Origin::Create(GURL("https://example.com")).DeriveNewOpaqueOrigin(),
+ GURL("http://example.com")));
+
+ // Other combinations do not.
+ EXPECT_FALSE(IsInsecureFormActionOnSecureSource(
+ url::Origin::Create(GURL("https://example.com")).DeriveNewOpaqueOrigin(),
+ GURL("https://example.com")));
+ EXPECT_FALSE(IsInsecureFormActionOnSecureSource(
+ url::Origin::Create(GURL("http://example.com")).DeriveNewOpaqueOrigin(),
+ GURL("http://example.com")));
+ EXPECT_FALSE(IsInsecureFormActionOnSecureSource(url::Origin(),
+ GURL("http://example.com")));
}
Regression Test / PoC
diff --git a/components/security_interstitials/core/insecure_form_util_unittest.cc b/components/security_interstitials/core/insecure_form_util_unittest.cc
index f62a178..3223b18 100644
--- a/components/security_interstitials/core/insecure_form_util_unittest.cc
+++ b/components/security_interstitials/core/insecure_form_util_unittest.cc
@@ -54,4 +54,19 @@
url::Origin::Create(GURL("http://127.0.0.1:123")),
GURL("http://example.com")));
#endif
+
+ // Opaque https source with insecure action still counts.
+ EXPECT_TRUE(IsInsecureFormActionOnSecureSource(
+ url::Origin::Create(GURL("https://example.com")).DeriveNewOpaqueOrigin(),
+ GURL("http://example.com")));
+
+ // Other combinations do not.
+ EXPECT_FALSE(IsInsecureFormActionOnSecureSource(
+ url::Origin::Create(GURL("https://example.com")).DeriveNewOpaqueOrigin(),
+ GURL("https://example.com")));
+ EXPECT_FALSE(IsInsecureFormActionOnSecureSource(
+ url::Origin::Create(GURL("http://example.com")).DeriveNewOpaqueOrigin(),
+ GURL("http://example.com")));
+ EXPECT_FALSE(IsInsecureFormActionOnSecureSource(url::Origin(),
+ GURL("http://example.com")));
}
Original Bug Report
InsecureFormNavigationThrottle bypass via opaque initiator origin
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A logic error in IsInsecureFormActionOnSecureSource fails to correctly identify secure origins when they are opaque, such as in sandboxed contexts. This allows an HTTPS page to potentially submit form data to an insecure HTTP endpoint without triggering the expected security interstitial warning.
Affected files:
components/security_interstitials/core/insecure_form_util.cccomponents/security_interstitials/content/insecure_form_navigation_throttle.cc
Estimated timestamp from git blame: 2020-10-06
Summary
Chrome’s InsecureFormNavigationThrottle is designed to protect users by displaying an interstitial warning when a secure (HTTPS) page attempts to submit a form to an insecure (HTTP) URL. This protection can potentially be bypassed if the initiating document has an opaque origin (e.g., due to a Content-Security-Policy: sandbox header or being inside a sandboxed <iframe>).
Technical Details
The issue resides in how the browser-side utility function IsInsecureFormActionOnSecureSource evaluates whether the initiator is a secure source. It checks the scheme of the origin directly using source_origin.scheme():
// components/security_interstitials/core/insecure_form_util.cc:44
bool IsInsecureFormActionOnSecureSource(const url::Origin& source_origin,
const GURL& action_url) {
if (!base::EqualsCaseInsensitiveASCII(source_origin.scheme(),
url::kHttpsScheme)) {
// On non-iOS platforms, returns false if scheme is not https
return false;
}
return IsInsecureFormAction(action_url);
}
For an opaque origin, url::Origin::scheme() is defined to return an empty string (url/origin.h:226-228), even if the origin has an HTTPS precursor. Consequently, the check source_origin.scheme() == url::kHttpsScheme fails, the function returns false, and the InsecureFormNavigationThrottle permits the navigation to proceed without showing the warning interstitial (components/security_interstitials/content/insecure_form_navigation_throttle.cc:132-136).
Impact
In its default configuration, Chrome does not degrade the ‘SECURE’ lock icon for mixed forms, relying instead on the submission-time interstitial to warn users. Because this interstitial is bypassed for opaque origins, a malicious HTTPS site could host a sandboxed page that appears secure but transmits sensitive user data (e.g., passwords or payment information) over unencrypted HTTP. Passive network observers could then intercept this data. Other protections, such as HttpsUpgradesInterceptor, do not apply to POST requests, and browser-side mixed content checks typically exempt main-frame navigations.
Potential Reproduction Steps
- Host a page over HTTPS with the following header:
Content-Security-Policy: sandbox allow-forms allow-scripts. - Include a form targeting an HTTP endpoint:
<form method='POST' action='http://example.com/collect'><input type='password' name='p' value='secret'></form>. - Trigger the form submission (e.g., via user interaction or script).
- Observe that the navigation proceeds directly over HTTP without a security interstitial, while the omnibox continues to display the secure lock icon.
Note: These steps are based on static code analysis; our tooling does not currently have the capability to run code to confirm the behavior.
Suggested Fix
The check in IsInsecureFormActionOnSecureSource should consider the precursor origin for opaque origins. This can be achieved by using GetTupleOrPrecursorTupleIfOpaque() instead of scheme(), which would align the browser-side logic with Blink’s MixedContentChecker:
if (!base::EqualsCaseInsensitiveASCII(source_origin.GetTupleOrPrecursorTupleIfOpaque().scheme(),
url::kHttpsScheme)) {
return false;
}
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.