High chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Omnibox
DescriptionIncorrect authorization in Omnibox
ComponentOmnibox
Sub-componentAim-drive
Bug ClassAuthorization Error
Tracker540018068
Fix commita5b1d40eaa17 (chromium/src) +203/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
if
chrome/browser/ui/webui/drive_picker_host/drive_picker_host_ui.cc
modified
TEST_F
chrome/browser/ui/webui/drive_picker_host/drive_picker_host_ui_unittest.cc
modified

Files Changed

  • chrome/browser/ui/webui/drive_picker_host/drive_picker_host_ui.cc
  • chrome/browser/ui/webui/drive_picker_host/drive_picker_host_ui_unittest.cc
From a5b1d40eaa174bec1ef1e9ef9626ae48b409e93b Mon Sep 17 00:00:00 2001
From: Adam Yao <[email protected]>
Date: Fri, 11 Sep 2026 16:23:29 -0700
Subject: [PATCH] [aim-drive] Validate flow_id and active consent flow in DrivePickerHostUI

Previously, DrivePickerHostUI::HandlePrivacyFlowResult checked whether
result.has_flow_id() && result.flow_id() != expected_flow_id. Because
flow_id is an optional protobuf field in PrivacyFlowResult, an untrusted
or compromised chrome-untrusted://drive-picker-host renderer could craft
a message omitting flow_id.

This caused has_flow_id() to evaluate to false, short-circuiting past
the validation check and allowing the renderer to bypass user consent,
update contextual_search::kDriveConsentState to kConsent, and acquire a
drive.readonly OAuth token. This CL addresses the vulnerability by:

1. Strictly requiring flow_id to be present and to match
   kComposeboxDriveConsentFlowId. Missing or mismatched flow IDs now
   trigger OnConsentKitError and abort processing.
2. Dropping unsolicited PrivacyFlowResult messages when no consent flow
   is currently outstanding (!consent_result_handler_.is_bound()),
   preventing unsolicited messages from altering profile preferences.
3. Notifying and resetting consent_result_handler_ with
kMojoDisconnected if untrusted_bridge_remote_ disconnects during an
active consent flow. 4. Adding regression unit tests in
DrivePickerHostUITest covering missing flow IDs, invalid flow IDs,
unsolicited results, and bridge disconnections.

Fixed: b:540018068
Change-Id: If8478b1a70ec58fe814c96475aa08edde2e2beca
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8401387
Reviewed-by: Juliet Knighton <[email protected]>
Commit-Queue: Adam Yao <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1696709}
---
diff --git a/chrome/browser/ui/webui/drive_picker_host/drive_picker_host_ui.cc b/chrome/browser/ui/webui/drive_picker_host/drive_picker_host_ui.cc
index cac1b31..77c0d2d 100644
--- a/chrome/browser/ui/webui/drive_picker_host/drive_picker_host_ui.cc
+++ b/chrome/browser/ui/webui/drive_picker_host/drive_picker_host_ui.cc
@@ -176,13 +176,21 @@
   untrusted_bridge_remote_.Bind(std::move(untrusted_bridge));
   untrusted_bridge_remote_.set_disconnect_handler(base::BindOnce(
       [](base::WeakPtr<DrivePickerHostUI> self) {
-        if (self && self->pending_request_ &&
+        if (!self) {
+          return;
+        }
+        if (self->pending_request_ &&
             self->pending_request_->has_result_handler()) {
           mojo::Remote<drive_picker_host::mojom::DrivePickerResultHandler>(
               self->pending_request_->TakeResultHandler())
               ->OnError(drive_picker_host::mojom::DrivePickerError::
                             kMojoDisconnected);
         }
+        if (self->consent_result_handler_) {
+          self->consent_result_handler_->OnError(
+              drive_picker_host::mojom::DrivePickerError::kMojoDisconnected);
+          self->consent_result_handler_.reset();
+        }
       },
       weak_ptr_factory_.GetWeakPtr()));
 
@@ -338,7 +346,13 @@
       << result.has_flow_completed()
       << ", flow_not_completed: " << result.has_flow_not_completed()
       << ", decisions: " << result.decision_size();
-  if (result.has_flow_id() &&
+  if (!consent_result_handler_.is_bound()) {
+    DLOG(WARNING)
+        << "PrivacyFlowResult received with no outstanding consent flow";
+    return;
+  }
+
+  if (!result.has_flow_id() ||
       result.flow_id() != static_cast<identity_consent::ConsentFlowId>(
                               omnibox::kComposeboxDriveConsentFlowId.Get())) {
     DLOG(WARNING) << "Unexpected or missing flow_id";
diff --git a/chrome/browser/ui/webui/drive_picker_host/drive_picker_host_ui_unittest.cc b/chrome/browser/ui/webui/drive_picker_host/drive_picker_host_ui_unittest.cc
index 6bd0012..098f77b7 100644
--- a/chrome/browser/ui/webui/drive_picker_host/drive_picker_host_ui_unittest.cc
+++ b/chrome/browser/ui/webui/drive_picker_host/drive_picker_host_ui_unittest.cc
@@ -56,6 +56,8 @@
       (override));
   MOCK_METHOD(void, LoadConsentKitUrl, (const GURL&), (override));
 
+  void Disconnect() { receiver_.reset(); }
+
  private:
   mojo::Receiver<drive_picker_host_untrusted::mojom::DrivePickerBridge>
       receiver_{this};
@@ -515,3 +517,188 @@
       profile()->GetPrefs()->GetInteger(contextual_search::kDriveConsentState),
       static_cast<int>(contextual_search::DriveConsentState::kConsent));
 }
+
+TEST_F(DrivePickerHostUITest, ConsentKitFlowMissingFlowId_RelaysError) {
+  feature_list_.InitAndEnableFeature(
+      omnibox::kComposeboxDriveContextMenuOption);
+
+  content::TestWebUI test_web_ui;
+  test_web_ui.set_web_contents(web_contents());
+  DrivePickerHostUI controller(&test_web_ui);
+
+  MockDrivePickerBridge mock_bridge;
+  controller.SetBridge(mock_bridge.BindAndGetRemote());
+
+  MockResultHandler result_handler;
+  auto request = std::make_unique<drive_picker_host::DrivePickerHostRequest>(
+      drive_picker_host::DrivePickerHostRequest::RequestType::kConsentDialog,
+      result_handler.BindAndGetRemote());
+
+  base::RunLoop consent_url_run_loop;
+  EXPECT_CALL(mock_bridge, LoadConsentKitUrl(testing::_))
+      .WillOnce(testing::InvokeWithoutArgs(
+          [&consent_url_run_loop]() { consent_url_run_loop.Quit(); }));
+  controller.TriggerDrivePickerHost(std::move(request));
+  consent_url_run_loop.Run();
+
+  // Initial consent preference should not be kConsent.
+  EXPECT_NE(
+      profile()->GetPrefs()->GetInteger(contextual_search::kDriveConsentState),
+      static_cast<int>(contextual_search::DriveConsentState::kConsent));
+
+  // ShowDrivePicker should never be called.
+  EXPECT_CALL(mock_bridge, ShowDrivePicker(testing::_, testing::_)).Times(0);
+
+  // Construct message with flow_completed and DECISION_CONSENT, but NO flow_id.
+  identity_consent::IframeMessage message;
+  message.set_event(identity_consent::Event::DECISION_RESPONSE_EVENT);
+  message.mutable_privacy_flow_result()->mutable_flow_completed();
+  auto* decision = message.mutable_privacy_flow_result()->add_decision();
+  decision->set_ftc_consent_setting_id(
+      identity_consent::ConsentSettingId::
+          PERSONAL_CONTEXT_SEARCH_USING_WORKSPACE);
+  decision->set_decision(identity_consent::Decision::DECISION_CONSENT);
+
+  base::RunLoop error_run_loop;
+  EXPECT_CALL(result_handler,
+              OnError(drive_picker_host::mojom::DrivePickerError::kUnknown))
+      .WillOnce(testing::InvokeWithoutArgs(
+          [&error_run_loop]() { error_run_loop.Quit(); }));
+
+  controller.OnConsentKitIframeMessage(mojo_base::ProtoWrapper(message));
+  error_run_loop.Run();
+
+  // Preference should remain not kConsent.
+  EXPECT_NE(
+      profile()->GetPrefs()->GetInteger(contextual_search::kDriveConsentState),
+      static_cast<int>(contextual_search::DriveConsentState::kConsent));
+}
+
+TEST_F(DrivePickerHostUITest, ConsentKitFlowMismatchedFlowId_RelaysError) {
+  feature_list_.InitAndEnableFeature(
+      omnibox::kComposeboxDriveContextMenuOption);
+
+  content::TestWebUI test_web_ui;
+  test_web_ui.set_web_contents(web_contents());
+  DrivePickerHostUI controller(&test_web_ui);
+
+  MockDrivePickerBridge mock_bridge;
+  controller.SetBridge(mock_bridge.BindAndGetRemote());
+
+  MockResultHandler result_handler;
+  auto request = std::make_unique<drive_picker_host::DrivePickerHostRequest>(
+      drive_picker_host::DrivePickerHostRequest::RequestType::kConsentDialog,
+      result_handler.BindAndGetRemote());
+
+  base::RunLoop consent_url_run_loop;
+  EXPECT_CALL(mock_bridge, LoadConsentKitUrl(testing::_))
+      .WillOnce(testing::InvokeWithoutArgs(
+          [&consent_url_run_loop]() { consent_url_run_loop.Quit(); }));
+  controller.TriggerDrivePickerHost(std::move(request));
+  consent_url_run_loop.Run();
+
+  // Initial consent preference should not be kConsent.
+  EXPECT_NE(
+      profile()->GetPrefs()->GetInteger(contextual_search::kDriveConsentState),
+      static_cast<int>(contextual_search::DriveConsentState::kConsent));
+
+  // ShowDrivePicker should never be called.
+  EXPECT_CALL(mock_bridge, ShowDrivePicker(testing::_, testing::_)).Times(0);
+
+  // Construct message with flow_completed and DECISION_CONSENT, but an invalid
+  // flow_id.
+  identity_consent::IframeMessage message;
+  message.set_event(identity_consent::Event::DECISION_RESPONSE_EVENT);
+  message.mutable_privacy_flow_result()->mutable_flow_completed();
+  message.mutable_privacy_flow_result()->set_flow_id(
+      identity_consent::ConsentFlowId::CONSENT_FLOW_ID_UNSPECIFIED);
+  auto* decision = message.mutable_privacy_flow_result()->add_decision();
+  decision->set_ftc_consent_setting_id(
+      identity_consent::ConsentSettingId::
+          PERSONAL_CONTEXT_SEARCH_USING_WORKSPACE);
+  decision->set_decision(identity_consent::Decision::DECISION_CONSENT);
+
+  base::RunLoop error_run_loop;
+  EXPECT_CALL(result_handler,
+              OnError(drive_picker_host::mojom::DrivePickerError::kUnknown))
+      .WillOnce(testing::InvokeWithoutArgs(
+          [&error_run_loop]() { error_run_loop.Quit(); }));
+
+  controller.OnConsentKitIframeMessage(mojo_base::ProtoWrapper(message));
+  error_run_loop.Run();
+
+  // Preference should remain not kConsent.
+  EXPECT_NE(
+      profile()->GetPrefs()->GetInteger(contextual_search::kDriveConsentState),
+      static_cast<int>(contextual_search::DriveConsentState::kConsent));
+}
+
+TEST_F(DrivePickerHostUITest,
+       ConsentKitFlowUnsolicitedResult_DroppedWithoutConsentOrPicker) {
+  feature_list_.InitAndEnableFeature(
+      omnibox::kComposeboxDriveContextMenuOption);
+
+  content::TestWebUI test_web_ui;
+  test_web_ui.set_web_contents(web_contents());
+  DrivePickerHostUI controller(&test_web_ui);
+
+  MockDrivePickerBridge mock_bridge;
+  controller.SetBridge(mock_bridge.BindAndGetRemote());
+
+  // ShowDrivePicker should never be called.
+  EXPECT_CALL(mock_bridge, ShowDrivePicker(testing::_, testing::_)).Times(0);
+
+  // Send an unsolicited PrivacyFlowResult directly when no consent flow was
+  // requested (consent_result_handler_ is unbound).
+  identity_consent::PrivacyFlowResult result;
+  result.mutable_flow_completed();
+  result.set_flow_id(static_cast<identity_consent::ConsentFlowId>(
+      omnibox::kComposeboxDriveConsentFlowId.Get()));
+  auto* decision = result.add_decision();
+  decision->set_ftc_consent_setting_id(
+      identity_consent::ConsentSettingId::
+          PERSONAL_CONTEXT_SEARCH_USING_WORKSPACE);
+  decision->set_decision(identity_consent::Decision::DECISION_CONSENT);
+
+  controller.OnConsentKitPrivacyFlowResult(mojo_base::ProtoWrapper(result));
+
+  // Preference must not be modified by unsolicited result.
+  EXPECT_NE(
+      profile()->GetPrefs()->GetInteger(contextual_search::kDriveConsentState),
+      static_cast<int>(contextual_search::DriveConsentState::kConsent));
+}
+
+TEST_F(DrivePickerHostUITest,
+       UntrustedBridgeDisconnect_RelaysErrorToConsentResultHandler) {
+  feature_list_.InitAndEnableFeature(
+      omnibox::kComposeboxDriveContextMenuOption);
+
+  content::TestWebUI test_web_ui;
+  test_web_ui.set_web_contents(web_contents());
+  DrivePickerHostUI controller(&test_web_ui);
+
+  MockDrivePickerBridge mock_bridge;
+  controller.SetBridge(mock_bridge.BindAndGetRemote());
+
+  MockResultHandler result_handler;
+  auto request = std::make_unique<drive_picker_host::DrivePickerHostRequest>(
+      drive_picker_host::DrivePickerHostRequest::RequestType::kConsentDialog,
+      result_handler.BindAndGetRemote());
+
+  base::RunLoop consent_url_run_loop;
+  EXPECT_CALL(mock_bridge, LoadConsentKitUrl(testing::_))
+      .WillOnce(testing::InvokeWithoutArgs(
+          [&consent_url_run_loop]() { consent_url_run_loop.Quit(); }));
+  controller.TriggerDrivePickerHost(std::move(request));
+  consent_url_run_loop.Run();
+
+  base::RunLoop disconnect_run_loop;
+  EXPECT_CALL(
+      result_handler,
+      OnError(drive_picker_host::mojom::DrivePickerError::kMojoDisconnected))
+      .WillOnce(testing::InvokeWithoutArgs(
+          [&disconnect_run_loop]() { disconnect_run_loop.Quit(); }));
+
+  mock_bridge.Disconnect();
+  disconnect_run_loop.Run();
+}
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ui/webui/drive_picker_host/drive_picker_host_ui_unittest.cc b/chrome/browser/ui/webui/drive_picker_host/drive_picker_host_ui_unittest.cc
index 6bd0012..098f77b7 100644
--- a/chrome/browser/ui/webui/drive_picker_host/drive_picker_host_ui_unittest.cc
+++ b/chrome/browser/ui/webui/drive_picker_host/drive_picker_host_ui_unittest.cc
@@ -56,6 +56,8 @@
       (override));
   MOCK_METHOD(void, LoadConsentKitUrl, (const GURL&), (override));
 
+  void Disconnect() { receiver_.reset(); }
+
  private:
   mojo::Receiver<drive_picker_host_untrusted::mojom::DrivePickerBridge>
       receiver_{this};
@@ -515,3 +517,188 @@
       profile()->GetPrefs()->GetInteger(contextual_search::kDriveConsentState),
       static_cast<int>(contextual_search::DriveConsentState::kConsent));
 }
+
+TEST_F(DrivePickerHostUITest, ConsentKitFlowMissingFlowId_RelaysError) {
+  feature_list_.InitAndEnableFeature(
+      omnibox::kComposeboxDriveContextMenuOption);
+
+  content::TestWebUI test_web_ui;
+  test_web_ui.set_web_contents(web_contents());
+  DrivePickerHostUI controller(&test_web_ui);
+
+  MockDrivePickerBridge mock_bridge;
+  controller.SetBridge(mock_bridge.BindAndGetRemote());
+
+  MockResultHandler result_handler;
+  auto request = std::make_unique<drive_picker_host::DrivePickerHostRequest>(
+      drive_picker_host::DrivePickerHostRequest::RequestType::kConsentDialog,
+      result_handler.BindAndGetRemote());
+
+  base::RunLoop consent_url_run_loop;
+  EXPECT_CALL(mock_bridge, LoadConsentKitUrl(testing::_))
+      .WillOnce(testing::InvokeWithoutArgs(
+          [&consent_url_run_loop]() { consent_url_run_loop.Quit(); }));
+  controller.TriggerDrivePickerHost(std::move(request));
+  consent_url_run_loop.Run();
+
+  // Initial consent preference should not be kConsent.
+  EXPECT_NE(
+      profile()->GetPrefs()->GetInteger(contextual_search::kDriveConsentState),
+      static_cast<int>(contextual_search::DriveConsentState::kConsent));
+
+  // ShowDrivePicker should never be called.
+  EXPECT_CALL(mock_bridge, ShowDrivePicker(testing::_, testing::_)).Times(0);
+
+  // Construct message with flow_completed and DECISION_CONSENT, but NO flow_id.
+  identity_consent::IframeMessage message;
+  message.set_event(identity_consent::Event::DECISION_RESPONSE_EVENT);
+  message.mutable_privacy_flow_result()->mutable_flow_completed();
+  auto* decision = message.mutable_privacy_flow_result()->add_decision();
+  decision->set_ftc_consent_setting_id(
+      identity_consent::ConsentSettingId::
+          PERSONAL_CONTEXT_SEARCH_USING_WORKSPACE);
+  decision->set_decision(identity_consent::Decision::DECISION_CONSENT);
+
+  base::RunLoop error_run_loop;
+  EXPECT_CALL(result_handler,
+              OnError(drive_picker_host::mojom::DrivePickerError::kUnknown))
+      .WillOnce(testing::InvokeWithoutArgs(
+          [&error_run_loop]() { error_run_loop.Quit(); }));
+
+  controller.OnConsentKitIframeMessage(mojo_base::ProtoWrapper(message));
+  error_run_loop.Run();
+
+  // Preference should remain not kConsent.
+  EXPECT_NE(
+      profile()->GetPrefs()->GetInteger(contextual_search::kDriveConsentState),
+      static_cast<int>(contextual_search::DriveConsentState::kConsent));
+}
+
+TEST_F(DrivePickerHostUITest, ConsentKitFlowMismatchedFlowId_RelaysError) {
+  feature_list_.InitAndEnableFeature(
+      omnibox::kComposeboxDriveContextMenuOption);
+
+  content::TestWebUI test_web_ui;
+  test_web_ui.set_web_contents(web_contents());
+  DrivePickerHostUI controller(&test_web_ui);
+
+  MockDrivePickerBridge mock_bridge;
+  controller.SetBridge(mock_bridge.BindAndGetRemote());
+
+  MockResultHandler result_handler;
+  auto request = std::make_unique<drive_picker_host::DrivePickerHostRequest>(
+      drive_picker_host::DrivePickerHostRequest::RequestType::kConsentDialog,
+      result_handler.BindAndGetRemote());
+
+  base::RunLoop consent_url_run_loop;
+  EXPECT_CALL(mock_bridge, LoadConsentKitUrl(testing::_))
+      .WillOnce(testing::InvokeWithoutArgs(
+          [&consent_url_run_loop]() { consent_url_run_loop.Quit(); }));
+  controller.TriggerDrivePickerHost(std::move(request));
+  consent_url_run_loop.Run();
+
+  // Initial consent preference should not be kConsent.
+  EXPECT_NE(
+      profile()->GetPrefs()->GetInteger(contextual_search::kDriveConsentState),
+      static_cast<int>(contextual_search::DriveConsentState::kConsent));
+
+  // ShowDrivePicker should never be called.
+  EXPECT_CALL(mock_bridge, ShowDrivePicker(testing::_, testing::_)).Times(0);
+
+  // Construct message with flow_completed and DECISION_CONSENT, but an invalid
+  // flow_id.
+  identity_consent::IframeMessage message;
+  message.set_event(identity_consent::Event::DECISION_RESPONSE_EVENT);
+  message.mutable_privacy_flow_result()->mutable_flow_completed();
+  message.mutable_privacy_flow_result()->set_flow_id(
+      identity_consent::ConsentFlowId::CONSENT_FLOW_ID_UNSPECIFIED);
+  auto* decision = message.mutable_privacy_flow_result()->add_decision();
+  decision->set_ftc_consent_setting_id(
+      identity_consent::ConsentSettingId::
+          PERSONAL_CONTEXT_SEARCH_USING_WORKSPACE);
+  decision->set_decision(identity_consent::Decision::DECISION_CONSENT);
+
+  base::RunLoop error_run_loop;
+  EXPECT_CALL(result_handler,
+              OnError(drive_picker_host::mojom::DrivePickerError::kUnknown))
+      .WillOnce(testing::InvokeWithoutArgs(
+          [&error_run_loop]() { error_run_loop.Quit(); }));
+
+  controller.OnConsentKitIframeMessage(mojo_base::ProtoWrapper(message));
+  error_run_loop.Run();
+
+  // Preference should remain not kConsent.
+  EXPECT_NE(
+      profile()->GetPrefs()->GetInteger(contextual_search::kDriveConsentState),
+      static_cast<int>(contextual_search::DriveConsentState::kConsent));
+}
+
+TEST_F(DrivePickerHostUITest,
+       ConsentKitFlowUnsolicitedResult_DroppedWithoutConsentOrPicker) {
+  feature_list_.InitAndEnableFeature(
+      omnibox::kComposeboxDriveContextMenuOption);
+
+  content::TestWebUI test_web_ui;
+  test_web_ui.set_web_contents(web_contents());
+  DrivePickerHostUI controller(&test_web_ui);
+
+  MockDrivePickerBridge mock_bridge;
+  controller.SetBridge(mock_bridge.BindAndGetRemote());
+
+  // ShowDrivePicker should never be called.
+  EXPECT_CALL(mock_bridge, ShowDrivePicker(testing::_, testing::_)).Times(0);
+
+  // Send an unsolicited PrivacyFlowResult directly when no consent flow was
+  // requested (consent_result_handler_ is unbound).
+  identity_consent::PrivacyFlowResult result;
+  result.mutable_flow_completed();
+  result.set_flow_id(static_cast<identity_consent::ConsentFlowId>(
+      omnibox::kComposeboxDriveConsentFlowId.Get()));
+  auto* decision = result.add_decision();
+  decision->set_ftc_consent_setting_id(
+      identity_consent::ConsentSettingId::
+          PERSONAL_CONTEXT_SEARCH_USING_WORKSPACE);
+  decision->set_decision(identity_consent::Decision::DECISION_CONSENT);
+
+  controller.OnConsentKitPrivacyFlowResult(mojo_base::ProtoWrapper(result));
+
+  // Preference must not be modified by unsolicited result.
+  EXPECT_NE(
+      profile()->GetPrefs()->GetInteger(contextual_search::kDriveConsentState),
+      static_cast<int>(contextual_search::DriveConsentState::kConsent));
+}
+
+TEST_F(DrivePickerHostUITest,
+       UntrustedBridgeDisconnect_RelaysErrorToConsentResultHandler) {
+  feature_list_.InitAndEnableFeature(
+      omnibox::kComposeboxDriveContextMenuOption);
+
+  content::TestWebUI test_web_ui;
+  test_web_ui.set_web_contents(web_contents());
+  DrivePickerHostUI controller(&test_web_ui);
+
+  MockDrivePickerBridge mock_bridge;
+  controller.SetBridge(mock_bridge.BindAndGetRemote());
+
+  MockResultHandler result_handler;
+  auto request = std::make_unique<drive_picker_host::DrivePickerHostRequest>(
+      drive_picker_host::DrivePickerHostRequest::RequestType::kConsentDialog,
+      result_handler.BindAndGetRemote());
+
+  base::RunLoop consent_url_run_loop;
+  EXPECT_CALL(mock_bridge, LoadConsentKitUrl(testing::_))
+      .WillOnce(testing::InvokeWithoutArgs(
+          [&consent_url_run_loop]() { consent_url_run_loop.Quit(); }));
+  controller.TriggerDrivePickerHost(std::move(request));
+  consent_url_run_loop.Run();
+
+  base::RunLoop disconnect_run_loop;
+  EXPECT_CALL(
+      result_handler,
+      OnError(drive_picker_host::mojom::DrivePickerError::kMojoDisconnected))
+      .WillOnce(testing::InvokeWithoutArgs(
+          [&disconnect_run_loop]() { disconnect_run_loop.Quit(); }));
+
+  mock_bridge.Disconnect();
+  disconnect_run_loop.Run();
+}
Loading diff…

Original Bug Report

reported by [email protected]

Confused-deputy consent bypass and OAuth token leak in DrivePickerHostUI

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 potential confused-deputy logic bug in DrivePickerHostUI allows a compromised chrome-untrusted://drive-picker-host renderer to bypass user consent validation. By sending a forged protobuf message omitting the optional flow_id, the renderer can persistently update the user’s consent state preference. This can trigger the browser to mint and disclose a highly privileged drive.readonly OAuth token to the untrusted renderer.

Affected files:

  • chrome/browser/ui/webui/drive_picker_host/drive_picker_host_ui.cc
  • chrome/browser/ui/webui/drive_picker_host/untrusted/drive_picker_host_untrusted_ui.cc

Estimated timestamp from git blame: 2026-07-08

Description and Root Cause

A potential logical security boundary bypass exists in the browser-process class DrivePickerHostUI. When handling the consent decision result from the ConsentKit flow, DrivePickerHostUI::HandlePrivacyFlowResult parses and evaluates an identity_consent::PrivacyFlowResult protobuf message supplied by the chrome-untrusted://drive-picker-host renderer process.

However, the safety guard checking the integrity of the flow identifier is implemented as follows in chrome/browser/ui/webui/drive_picker_host/drive_picker_host_ui.cc:

void DrivePickerHostUI::HandlePrivacyFlowResult(
    const identity_consent::PrivacyFlowResult& result) {
  ...
  if (result.has_flow_id() &&
      result.flow_id().value() !=
          omnibox::kComposeboxDriveConsentFlowId.Get()) {
    DLOG(WARNING) << "Unexpected or missing flow_id";
    OnConsentKitError("Unexpected or missing flow_id");
    return;
  }

Because flow_id is defined as optional in components/contextual_search/consent_kit/proto/iframe_interface.proto, an attacker can craft a serialized protobuf message that omits the flow_id field. Under this condition, result.has_flow_id() evaluates to false, causing the check to short-circuit and entirely bypass the early return block.

If the forged protobuf also contains flow_completed (ConsentFlowCompleted), the browser process will trust the decision, permanently write contextual_search::kDriveConsentState as kConsent to profile preferences, and proceed to fetch and deliver a drive.readonly OAuth bearer token to the untrusted renderer.


Potential Attack Flow

Note: The following step-by-step sequence represents potential steps that an attacker would follow to trigger this vulnerability. Our automated security tooling agent does not currently have the ability to run code or compile a working proof of concept.

  1. Preconditions: The desktop field trial ComposeboxDriveContextMenuOptionDesktop is active (enabling omnibox::kComposeboxDriveContextMenuOption), the victim is logged into Chrome, and the attacker has achieved code execution inside the renderer process hosting chrome-untrusted://drive-picker-host.
  2. Trigger Dialog: The victim clicks the Google Drive context menu option inside the composebox. Since consent has not yet been given, the browser loads the DrivePickerHostUI which dynamically creates the untrusted iframe.
  3. Interface Linkage: DrivePickerHostUI::MaybeBindUntrustedBridge (drive_picker_host_ui.cc:129-148) binds the delegate relationship and initializes the Mojo communication channels.
  4. Forged IPC: The compromised renderer bypasses the user interaction phase and directly sends an IPC Mojo message PageHandler.OnConsentKitPrivacyFlowResult with a custom mojo_base.mojom.ProtoWrapper wrapping an identity_consent.PrivacyFlowResult structure.
  5. Bypassing Verification: The attacker omits the flow_id field but populates the flow_completed submessage structure.
  6. State Corruption: The browser process parses the message in DrivePickerHostUI::OnConsentKitPrivacyFlowResult, passes the check in HandlePrivacyFlowResult, and persistently writes contextual_search::kDriveConsentState to DriveConsentState::kConsent.
  7. Token Leaking: The browser invokes FetchTokenAndShowPicker (drive_picker_host_ui.cc:200-229), obtaining a valid OAuth bearer token (with scope https://www.googleapis.com/auth/drive.readonly via signin::OAuthConsumerId::kDrivePickerHost).
  8. Exfiltration: The browser passes the minted bearer token back to the untrusted renderer via untrusted_bridge_remote_->ShowDrivePicker(..., std::move(keys)) which crosses the IPC boundary. The attacker now possesses full read access to the victim’s Google Drive files.

Security Impact

  • Credential Leakage: A compromised renderer is granted a highly sensitive, long-lived OAuth credential that exposes the user’s entire Google Drive, in direct violation of Chrome’s threat model guidelines (docs/webui/chrome_untrusted.md).
  • Syncable Preference Poisoning: The written consent state is a syncable profile preference. Once corrupted, the fake consent propagates across all of the user’s synced devices, bypassing future consent checks entirely and facilitating recurring token leaks on other platforms.

Suggested Fix

Modify DrivePickerHostUI::HandlePrivacyFlowResult to strictly require the presence of flow_id and ensure that it matches the expected product flow identifier. Reject the message if flow_id is missing:

  if (!result.has_flow_id() ||
      result.flow_id().value() !=
          omnibox::kComposeboxDriveConsentFlowId.Get()) {
    DLOG(WARNING) << "Unexpected or missing flow_id";
    OnConsentKitError("Unexpected or missing flow_id");
    return;
  }

Evaluated with Chrome root at commit: 94d9235ebe3b7276e5284f0dc5d55577ff949908


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.

View on issue tracker