Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Password Manager
DescriptionInsufficient policy enforcement in Password Manager
ComponentPassword Manager
Bug ClassLogic Error
Tracker496702621
Fix commitb17a351979ab (chromium/src) +66/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
MockPasswordManagerClient
components/password_manager/core/browser/password_form_filling_unittest.cc
modified
TEST_F
components/password_manager/core/browser/password_form_filling_unittest.cc
modified
MockPasswordManagerClient
components/password_manager/core/browser/password_form_manager_unittest.cc
modified

Files Changed

  • chrome/browser/password_manager/actor_login/internal/actor_login_delegate_impl_unittest.cc
  • components/password_manager/content/browser/content_password_manager_driver.cc
  • components/password_manager/core/browser/password_form_filling.cc
  • components/password_manager/core/browser/password_form_filling_unittest.cc
  • components/password_manager/core/browser/password_form_manager_unittest.cc
From b17a351979abda6234f005fdccedd91d88929394 Mon Sep 17 00:00:00 2001
From: Vasilii Sukhanov <[email protected]>
Date: Wed, 08 Apr 2026 13:05:52 -0700
Subject: [PATCH] Fix potential CPSP bypass in PasswordManager

Enforce ChildProcessSecurityPolicy in HasValidURL and strengthen origin
checks in password_form_filling.cc by comparing main frame origin with
driver origin.

Fixed: 496702621
Change-Id: Ie1932943ab234a8896a6137b3704e9032e5855c3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7735124
Reviewed-by: Ioana Treib <[email protected]>
Commit-Queue: Vasilii Sukhanov <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1611740}
---

diff --git a/chrome/browser/password_manager/actor_login/internal/actor_login_delegate_impl_unittest.cc b/chrome/browser/password_manager/actor_login/internal/actor_login_delegate_impl_unittest.cc
index ba4813566..6f97ae3c 100644
--- a/chrome/browser/password_manager/actor_login/internal/actor_login_delegate_impl_unittest.cc
+++ b/chrome/browser/password_manager/actor_login/internal/actor_login_delegate_impl_unittest.cc
@@ -194,6 +194,9 @@
   void SetUp() override {
     ChromeRenderViewHostTestHarness::SetUp();
 
+    ON_CALL(mock_driver_, GetLastCommittedOrigin())
+        .WillByDefault(ReturnRef(test_origin_));
+
     ActorLoginPermissionServiceFactory::GetInstance()->SetTestingFactory(
         profile(), base::BindRepeating([](content::BrowserContext* context)
                                            -> std::unique_ptr<KeyedService> {
@@ -315,6 +318,7 @@
   FakeFormFetcher form_fetcher_;
   std::vector<std::unique_ptr<PasswordFormManager>> form_managers_;
   NiceMock<MockPasswordManagerDriver> mock_driver_;
+  url::Origin test_origin_ = url::Origin::Create(GURL(kTestUrl));
   autofill::test::AutofillUnitTestEnvironment autofill_test_environment_{
       {.disable_server_communication = true}};
   NiceMock<MockActorLoginQualityLogger> mock_mqls_logger;
diff --git a/components/password_manager/content/browser/content_password_manager_driver.cc b/components/password_manager/content/browser/content_password_manager_driver.cc
index 8a3eab5..542c2ab 100644
--- a/components/password_manager/content/browser/content_password_manager_driver.cc
+++ b/components/password_manager/content/browser/content_password_manager_driver.cc
@@ -60,12 +60,16 @@
   if (!url.is_valid())
     return false;
 
-  return password_manager::bad_message::CheckForIllegalURL(
+  return password_manager::bad_message::CheckChildProcessSecurityPolicyForURL(
       render_frame_host, url,
       password_manager::BadMessageReason::CPMD_BAD_ORIGIN_FORM_SUBMITTED);
 }
 
 bool IsRenderFrameHostSupported(content::RenderFrameHost* rfh) {
+  if (rfh->GetProcess()->IsPdf()) {
+    return false;
+  }
+
   // Explanation of current PasswordManagerDriver limitations:
   // * Currently, PasswordManagerDriver binding has RenderFrameHost lifetime,
   //   not document lifetime. This can lead to premature binding in rare race
diff --git a/components/password_manager/core/browser/password_form_filling.cc b/components/password_manager/core/browser/password_form_filling.cc
index 70f0c2d..49961692 100644
--- a/components/password_manager/core/browser/password_form_filling.cc
+++ b/components/password_manager/core/browser/password_form_filling.cc
@@ -235,8 +235,8 @@
   } else if (preferred_match &&
              GetMatchType(*preferred_match) == GetLoginMatchType::kGrouped) {
     wait_for_username_reason = WaitForUsernameReason::kGroupedMatch;
-  } else if (!IsSameOrigin(client->GetLastCommittedOrigin(),
-                           GURL(observed_form.signon_realm))) {
+  } else if (!client->GetLastCommittedOrigin().IsSameOriginWith(
+                 driver->GetLastCommittedOrigin())) {
     wait_for_username_reason = WaitForUsernameReason::kCrossOriginIframe;
   } else if (not_sign_in_form) {
     // If the parser did not find a current password element, don't fill.
diff --git a/components/password_manager/core/browser/password_form_filling_unittest.cc b/components/password_manager/core/browser/password_form_filling_unittest.cc
index f6fbdfc..247d0a06 100644
--- a/components/password_manager/core/browser/password_form_filling_unittest.cc
+++ b/components/password_manager/core/browser/password_form_filling_unittest.cc
@@ -39,6 +39,7 @@
 using autofill::PasswordFormFillData;
 using testing::_;
 using testing::Return;
+using testing::ReturnRef;
 using testing::SaveArg;
 using url::Origin;
 using Store = password_manager::PasswordForm::Store;
@@ -57,6 +58,10 @@
               (const PasswordFormFillData&),
               (override));
   MOCK_METHOD(void, InformNoSavedCredentials, (bool), (override));
+  MOCK_METHOD(const url::Origin&,
+              GetLastCommittedOrigin,
+              (),
+              (const, override));
 };
 
 class MockPasswordManagerClient : public StubPasswordManagerClient {
@@ -121,6 +126,9 @@
     ON_CALL(client_, GetLastCommittedOrigin())
         .WillByDefault(
             Return(Origin::Create(GURL("https://accounts.google.com"))));
+    driver_origin_ = Origin::Create(GURL("https://accounts.google.com"));
+    ON_CALL(driver_, GetLastCommittedOrigin())
+        .WillByDefault(ReturnRef(driver_origin_));
 
     observed_form_.url = GURL("https://accounts.google.com/a/LoginAuth");
     observed_form_.action = GURL("https://accounts.google.com/a/Login");
@@ -172,6 +180,7 @@
   const std::vector<PasswordForm> federated_matches_;
   MockWebAuthnCredentialsDelegate webauthn_credentials_delegate_;
   testing::NiceMock<MockPasswordFeatureManager> feature_manager_;
+  url::Origin driver_origin_;
 };
 
 TEST_F(PasswordFormFillingTest, NoSavedCredentials) {
@@ -406,6 +415,7 @@
   ON_CALL(client_, GetLastCommittedOrigin)
       .WillByDefault(
           Return(Origin::Create(GURL(observed_http_form.signon_realm))));
+  driver_origin_ = Origin::Create(GURL(observed_http_form.signon_realm));
 
   ASSERT_FALSE(GURL(saved_http_match.signon_realm).SchemeIsCryptographic());
   std::vector<PasswordForm> best_matches = {saved_http_match};
@@ -485,6 +495,32 @@
   ON_CALL(client_, GetLastCommittedOrigin)
       .WillByDefault(
           Return(Origin::Create(GURL("https://another_website.com"))));
+  driver_origin_ = Origin::Create(GURL("https://some_website.com"));
+
+  std::vector<PasswordForm> best_matches = {saved_match_};
+  const std::vector<PasswordForm> federated_matches = {};
+
+  LikelyFormFilling likely_form_filling = SendFillInformationToRenderer(
+      &client_, &driver_, observed_form_, best_matches, federated_matches,
+      &saved_match_, metrics_recorder_.get(),
+      /*webauthn_suggestions_available=*/false,
+      /*suggestion_banned_fields=*/{});
+  EXPECT_EQ(LikelyFormFilling::kFillOnAccountSelect, likely_form_filling);
+  histogram_tester.ExpectUniqueSample(
+      "PasswordManager.FirstWaitForUsernameReason",
+      PasswordFormMetricsRecorder::WaitForUsernameReason::kCrossOriginIframe,
+      1);
+}
+
+TEST_F(PasswordFormFillingTest, NoFillOnPageloadForOpaqueOrigin) {
+  base::HistogramTester histogram_tester;
+
+  observed_form_.url = GURL("https://some_website.com");
+  saved_match_.url = GURL("https://some_website.com");
+
+  url::Origin opaque_origin;
+  ON_CALL(driver_, GetLastCommittedOrigin)
+      .WillByDefault(ReturnRef(opaque_origin));
 
   std::vector<PasswordForm> best_matches = {saved_match_};
   const std::vector<PasswordForm> federated_matches = {};
diff --git a/components/password_manager/core/browser/password_form_manager_unittest.cc b/components/password_manager/core/browser/password_form_manager_unittest.cc
index d5e86e7..268fb7c 100644
--- a/components/password_manager/core/browser/password_form_manager_unittest.cc
+++ b/components/password_manager/core/browser/password_form_manager_unittest.cc
@@ -204,6 +204,10 @@
               (const autofill::PasswordFormGenerationData&),
               (override));
   MOCK_METHOD(bool, IsInPrimaryMainFrame, (), (const, override));
+  MOCK_METHOD(const url::Origin&,
+              GetLastCommittedOrigin,
+              (),
+              (const, override));
 };
 
 class MockPasswordManagerClient : public StubPasswordManagerClient {
@@ -432,6 +436,7 @@
     GURL psl_action = GURL("https://myaccounts.google.com/a/ServiceLogin");
 
     observed_form_.set_url(origin);
+    observed_origin_ = url::Origin::Create(origin);
     observed_form_.set_action(action);
     observed_form_.set_name(u"sign-in");
     observed_form_.set_renderer_id(FormRendererId(1));
@@ -523,6 +528,8 @@
         .WillByDefault(ReturnRef(observed_form_.url()));
     ON_CALL(client_, GetLastCommittedOrigin)
         .WillByDefault(Return(url::Origin::Create(observed_form_.url())));
+    ON_CALL(driver_, GetLastCommittedOrigin)
+        .WillByDefault(ReturnRef(observed_origin_));
     ON_CALL(client_, GetWebAuthnCredentialsDelegateForDriver)
         .WillByDefault(Return(&webauthn_credentials_delegate_));
     ON_CALL(webauthn_credentials_delegate_, GetPasskeys)
@@ -658,6 +665,7 @@
   }
 
   FormData observed_form_;
+  url::Origin observed_origin_;
   FormData submitted_form_;
   FormData observed_form_only_password_fields_;
   FormData non_password_form_;
diff --git a/components/password_manager/core/browser/password_manager_unittest.cc b/components/password_manager/core/browser/password_manager_unittest.cc
index 339864a6..53adbf3 100644
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/password_manager/actor_login/internal/actor_login_delegate_impl_unittest.cc b/chrome/browser/password_manager/actor_login/internal/actor_login_delegate_impl_unittest.cc
index ba4813566..6f97ae3c 100644
--- a/chrome/browser/password_manager/actor_login/internal/actor_login_delegate_impl_unittest.cc
+++ b/chrome/browser/password_manager/actor_login/internal/actor_login_delegate_impl_unittest.cc
@@ -194,6 +194,9 @@
   void SetUp() override {
     ChromeRenderViewHostTestHarness::SetUp();
 
+    ON_CALL(mock_driver_, GetLastCommittedOrigin())
+        .WillByDefault(ReturnRef(test_origin_));
+
     ActorLoginPermissionServiceFactory::GetInstance()->SetTestingFactory(
         profile(), base::BindRepeating([](content::BrowserContext* context)
                                            -> std::unique_ptr<KeyedService> {
@@ -315,6 +318,7 @@
   FakeFormFetcher form_fetcher_;
   std::vector<std::unique_ptr<PasswordFormManager>> form_managers_;
   NiceMock<MockPasswordManagerDriver> mock_driver_;
+  url::Origin test_origin_ = url::Origin::Create(GURL(kTestUrl));
   autofill::test::AutofillUnitTestEnvironment autofill_test_environment_{
       {.disable_server_communication = true}};
   NiceMock<MockActorLoginQualityLogger> mock_mqls_logger;
diff --git a/components/password_manager/core/browser/password_form_filling_unittest.cc b/components/password_manager/core/browser/password_form_filling_unittest.cc
index f6fbdfc..247d0a06 100644
--- a/components/password_manager/core/browser/password_form_filling_unittest.cc
+++ b/components/password_manager/core/browser/password_form_filling_unittest.cc
@@ -39,6 +39,7 @@
 using autofill::PasswordFormFillData;
 using testing::_;
 using testing::Return;
+using testing::ReturnRef;
 using testing::SaveArg;
 using url::Origin;
 using Store = password_manager::PasswordForm::Store;
@@ -57,6 +58,10 @@
               (const PasswordFormFillData&),
               (override));
   MOCK_METHOD(void, InformNoSavedCredentials, (bool), (override));
+  MOCK_METHOD(const url::Origin&,
+              GetLastCommittedOrigin,
+              (),
+              (const, override));
 };
 
 class MockPasswordManagerClient : public StubPasswordManagerClient {
@@ -121,6 +126,9 @@
     ON_CALL(client_, GetLastCommittedOrigin())
         .WillByDefault(
             Return(Origin::Create(GURL("https://accounts.google.com"))));
+    driver_origin_ = Origin::Create(GURL("https://accounts.google.com"));
+    ON_CALL(driver_, GetLastCommittedOrigin())
+        .WillByDefault(ReturnRef(driver_origin_));
 
     observed_form_.url = GURL("https://accounts.google.com/a/LoginAuth");
     observed_form_.action = GURL("https://accounts.google.com/a/Login");
@@ -172,6 +180,7 @@
   const std::vector<PasswordForm> federated_matches_;
   MockWebAuthnCredentialsDelegate webauthn_credentials_delegate_;
   testing::NiceMock<MockPasswordFeatureManager> feature_manager_;
+  url::Origin driver_origin_;
 };
 
 TEST_F(PasswordFormFillingTest, NoSavedCredentials) {
@@ -406,6 +415,7 @@
   ON_CALL(client_, GetLastCommittedOrigin)
       .WillByDefault(
           Return(Origin::Create(GURL(observed_http_form.signon_realm))));
+  driver_origin_ = Origin::Create(GURL(observed_http_form.signon_realm));
 
   ASSERT_FALSE(GURL(saved_http_match.signon_realm).SchemeIsCryptographic());
   std::vector<PasswordForm> best_matches = {saved_http_match};
@@ -485,6 +495,32 @@
   ON_CALL(client_, GetLastCommittedOrigin)
       .WillByDefault(
           Return(Origin::Create(GURL("https://another_website.com"))));
+  driver_origin_ = Origin::Create(GURL("https://some_website.com"));
+
+  std::vector<PasswordForm> best_matches = {saved_match_};
+  const std::vector<PasswordForm> federated_matches = {};
+
+  LikelyFormFilling likely_form_filling = SendFillInformationToRenderer(
+      &client_, &driver_, observed_form_, best_matches, federated_matches,
+      &saved_match_, metrics_recorder_.get(),
+      /*webauthn_suggestions_available=*/false,
+      /*suggestion_banned_fields=*/{});
+  EXPECT_EQ(LikelyFormFilling::kFillOnAccountSelect, likely_form_filling);
+  histogram_tester.ExpectUniqueSample(
+      "PasswordManager.FirstWaitForUsernameReason",
+      PasswordFormMetricsRecorder::WaitForUsernameReason::kCrossOriginIframe,
+      1);
+}
+
+TEST_F(PasswordFormFillingTest, NoFillOnPageloadForOpaqueOrigin) {
+  base::HistogramTester histogram_tester;
+
+  observed_form_.url = GURL("https://some_website.com");
+  saved_match_.url = GURL("https://some_website.com");
+
+  url::Origin opaque_origin;
+  ON_CALL(driver_, GetLastCommittedOrigin)
+      .WillByDefault(ReturnRef(opaque_origin));
 
   std::vector<PasswordForm> best_matches = {saved_match_};
   const std::vector<PasswordForm> federated_matches = {};
diff --git a/components/password_manager/core/browser/password_form_manager_unittest.cc b/components/password_manager/core/browser/password_form_manager_unittest.cc
index d5e86e7..268fb7c 100644
--- a/components/password_manager/core/browser/password_form_manager_unittest.cc
+++ b/components/password_manager/core/browser/password_form_manager_unittest.cc
@@ -204,6 +204,10 @@
               (const autofill::PasswordFormGenerationData&),
               (override));
   MOCK_METHOD(bool, IsInPrimaryMainFrame, (), (const, override));
+  MOCK_METHOD(const url::Origin&,
+              GetLastCommittedOrigin,
+              (),
+              (const, override));
 };
 
 class MockPasswordManagerClient : public StubPasswordManagerClient {
@@ -432,6 +436,7 @@
     GURL psl_action = GURL("https://myaccounts.google.com/a/ServiceLogin");
 
     observed_form_.set_url(origin);
+    observed_origin_ = url::Origin::Create(origin);
     observed_form_.set_action(action);
     observed_form_.set_name(u"sign-in");
     observed_form_.set_renderer_id(FormRendererId(1));
@@ -523,6 +528,8 @@
         .WillByDefault(ReturnRef(observed_form_.url()));
     ON_CALL(client_, GetLastCommittedOrigin)
         .WillByDefault(Return(url::Origin::Create(observed_form_.url())));
+    ON_CALL(driver_, GetLastCommittedOrigin)
+        .WillByDefault(ReturnRef(observed_origin_));
     ON_CALL(client_, GetWebAuthnCredentialsDelegateForDriver)
         .WillByDefault(Return(&webauthn_credentials_delegate_));
     ON_CALL(webauthn_credentials_delegate_, GetPasskeys)
@@ -658,6 +665,7 @@
   }
 
   FormData observed_form_;
+  url::Origin observed_origin_;
   FormData submitted_form_;
   FormData observed_form_only_password_fields_;
   FormData non_password_form_;
diff --git a/components/password_manager/core/browser/password_manager_unittest.cc b/components/password_manager/core/browser/password_manager_unittest.cc
index 339864a6..53adbf3 100644
--- a/components/password_manager/core/browser/password_manager_unittest.cc
+++ b/components/password_manager/core/browser/password_manager_unittest.cc
@@ -378,6 +378,10 @@
               (override));
   MOCK_METHOD(bool, IsInPrimaryMainFrame, (), (const, override));
   MOCK_METHOD(const GURL&, GetLastCommittedURL, (), (const, override));
+  MOCK_METHOD(const url::Origin&,
+              GetLastCommittedOrigin,
+              (),
+              (const, override));
   MOCK_METHOD(void,
               GeneratedPasswordAccepted,
               (const std::u16string& password),
@@ -483,6 +487,8 @@
         .WillByDefault(ReturnRef(test_form_url_));
     ON_CALL(client_, GetLastCommittedOrigin)
         .WillByDefault(Return(url::Origin::Create(test_form_url_)));
+    ON_CALL(driver_, GetLastCommittedOrigin)
+        .WillByDefault(ReturnRef(test_form_origin_));
     ON_CALL(client_, IsCommittedMainFrameSecure()).WillByDefault(Return(true));
     ON_CALL(client_, IsFillingEnabled).WillByDefault(Return(true));
     ON_CALL(client_, GetMetricsRecorder()).WillByDefault(Return(nullptr));
@@ -837,6 +843,7 @@
   }
 
   const GURL test_form_url_{"https://www.google.com/a/LoginAuth"};
+  const url::Origin test_form_origin_{url::Origin::Create(test_form_url_)};
   const GURL test_form_action_{"https://www.google.com/a/Login"};
   const std::string test_signon_realm_ = "https://www.google.com/";
   base::test::SingleThreadTaskEnvironment task_environment_{
@@ -2865,6 +2872,10 @@
 
   // Observe the form in the second frame.
   MockPasswordManagerDriver driver_b;
+  url::Origin test_origin =
+      url::Origin::Create(GURL("http://www.example.com/"));
+  ON_CALL(driver_b, GetLastCommittedOrigin)
+      .WillByDefault(ReturnRef(test_origin));
   EXPECT_CALL(driver_b, PropagateFillDataOnParsingCompletion);
   manager()->OnPasswordFormsParsed(&driver_b, {form_data2});
   task_environment_.RunUntilIdle();
Loading diff…

Original Bug Report

reported by [email protected]

Potential CPSP bypass in PasswordManager allows sandboxed iframes to leak credentials

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A missing ChildProcessSecurityPolicy check in ContentPasswordManagerDriver::PasswordFormsParsed allows a compromised sandboxed iframe process to request password autofill data. Because the browser compares the main frame’s origin with the iframe’s precursor URL instead of its opaque origin, it treats the request as same-origin, sending plaintext credentials back to the isolated sandboxed renderer.

Affected files:

  • components/password_manager/content/browser/content_password_manager_driver.cc
  • components/password_manager/content/browser/bad_message.cc
  • components/password_manager/core/browser/password_form_filling.cc
  • components/autofill/core/common/password_form_fill_data.cc
  • chrome/browser/password_manager/chrome_password_manager_client.cc
  • components/password_manager/content/browser/form_meta_data.cc

Estimated timestamp from git blame: 2024-06-25

Overview

Note: This is a potential vulnerability identified by an AI agent (Flapjack). The steps below are theoretical and have not been verified with a working proof-of-concept exploit, as the agent does not currently have code execution capabilities.

Chrome’s IsolateSandboxedIframes feature places sandboxed iframes into dedicated, isolated renderer processes. The ChildProcessSecurityPolicy (CPSP) correctly denies these opaque-origin processes access to sensitive data, such as passwords, belonging to their precursor origin.

However, a logic flaw in the Password Manager’s Mojo message handling allows a compromised sandboxed renderer to bypass this isolation and exfiltrate the precursor origin’s saved credentials.

There are two main factors contributing to this vulnerability:

  1. Missing CPSP Check: When the browser receives a PasswordFormsParsed Mojo message, ContentPasswordManagerDriver::PasswordFormsParsed validates the request using HasValidURL(). This function only calls bad_message::CheckForIllegalURL, which merely rejects about: and data: schemes. It crucially fails to call bad_message::CheckChildProcessSecurityPolicyForURL, meaning the browser never verifies if the requesting process is actually authorized to access data for the given URL.
  2. Cross-Origin Bypass: When deciding whether to autofill credentials immediately or wait for user interaction (wait_for_username), SendFillInformationToRenderer compares the primary main frame’s origin against the form’s signon_realm. For a sandboxed iframe, the signon_realm is derived from its precursor URL. If the main frame and the sandboxed iframe share the same precursor origin (e.g., https://victim.com), IsSameOrigin() evaluates to true. The browser incorrectly assumes the form is same-origin, bypasses the cross-origin wait requirement, and sends plaintext passwords to the renderer.

Potential Steps to Trigger

  1. A victim user saves their username and password for https://victim.com in Chrome.
  2. The victim visits https://victim.com, which embeds a sandboxed iframe: <iframe sandbox="allow-scripts" src="/sandboxed.html"></iframe>.
  3. The sandboxed iframe is placed in an isolated renderer process with an opaque origin.
  4. An attacker exploits a separate vulnerability (e.g., a V8 bug) to achieve Remote Code Execution (RCE) within the sandboxed iframe’s renderer process.
  5. The attacker’s code bypasses renderer-side checks (like FrameCanAccessPasswordManager) and crafts a fake autofill::FormData object mimicking a login form with a URL of https://victim.com/sandboxed.html.
  6. The compromised renderer sends a PasswordFormsParsed Mojo message to the browser process containing the fake form.
  7. ContentPasswordManagerDriver::PasswordFormsParsed validates the URL but misses the CPSP check, allowing the request to proceed.
  8. The browser fetches the saved credentials for https://victim.com from the Password Store.
  9. In SendFillInformationToRenderer, the browser compares the main frame’s origin (https://victim.com) to the form’s signon_realm (https://victim.com/). Since they match, wait_for_username evaluates to false.
  10. autofill::MaybeClearPasswordValues leaves the plaintext credentials intact because wait_for_username is false.
  11. The browser sends the plaintext username and password back to the compromised sandboxed renderer process via ApplyFillDataOnParsingCompletion, successfully exfiltrating the credentials.

Suggested Fix

  1. Enforce CPSP in HasValidURL: In components/password_manager/content/browser/content_password_manager_driver.cc, update HasValidURL to call password_manager::bad_message::CheckChildProcessSecurityPolicyForURL instead of CheckForIllegalURL. This will immediately terminate the compromised renderer if an opaque/sandboxed process attempts to access password data.

  2. Strengthen Origin Checks in password_form_filling.cc: When calculating wait_for_username_reason in PasswordFormFilling::SendFillInformationToRenderer, ensure that the origin comparison takes opaque origins into account. Comparing against the precursor URL’s signon_realm is insufficient if the frame is sandboxed.

Evaluated with Chrome root at commit: False


Results from Fortify so far have been promising, but it can be wrong in its deductions. At this time, it does not produce proof of concepts or fuzzer tests. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve Fortify’s accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker