Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Browser
DescriptionInsufficient validation of untrusted input in Browser
ComponentBrowser
Bug ClassLogic Error
Tracker513271007
Fix commit205b57ec1eb7 (chromium/src) +73/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
chrome/browser/ai/ai_language_model.cc
modified
AILanguageModelOpenLoopToolTest
chrome/browser/ai/ai_language_model_unittest.cc
modified
AILanguageModelOpenLoopToolTest
chrome/browser/ai/ai_language_model_unittest.cc
modified
TEST_F
chrome/browser/ai/ai_language_model_unittest.cc
modified

Files Changed

  • chrome/browser/ai/ai_language_model.cc
  • chrome/browser/ai/ai_language_model_unittest.cc
  • chrome/browser/ai/ai_manager.cc
From 205b57ec1eb7b9ab3bdb34a5666f5c229b5443e8 Mon Sep 17 00:00:00 2001
From: Mike Wasserman <[email protected]>
Date: Wed, 20 May 2026 13:33:40 -0700
Subject: [PATCH] Prompt API: Gate tool use requests by feature flag in browser

Add browser-side enforcement of renderer-side feature checks.
Add unit tests.

Fixed: 513271007
Change-Id: I162511571020fdb8abb55f9354d6d9fd6c28d8d2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7858163
Reviewed-by: Isaac Ahouma <[email protected]>
Auto-Submit: Mike Wasserman <[email protected]>
Commit-Queue: Mike Wasserman <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1633802}
---

diff --git a/chrome/browser/ai/ai_language_model.cc b/chrome/browser/ai/ai_language_model.cc
index a35583d2..9a715f6b 100644
--- a/chrome/browser/ai/ai_language_model.cc
+++ b/chrome/browser/ai/ai_language_model.cc
@@ -159,6 +159,9 @@
           // TODO(crbug.com/422803232): Support on_device_model tool use.
           return nullptr;
         case blink::mojom::AILanguageModelPromptContent::Tag::kToolResponse: {
+          if (!capabilities.Has(on_device_model::CapabilityFlags::kToolUse)) {
+            return nullptr;
+          }
           // TODO(crbug.com/422803232): Support image/audio result types.
           auto response = GetToolResponseFromDict(content->get_tool_response());
           if (!response) {
diff --git a/chrome/browser/ai/ai_language_model_unittest.cc b/chrome/browser/ai/ai_language_model_unittest.cc
index db0fdf45..3a9f58e 100644
--- a/chrome/browser/ai/ai_language_model_unittest.cc
+++ b/chrome/browser/ai/ai_language_model_unittest.cc
@@ -1827,6 +1827,12 @@
 
 // Test class for `Tool Use` functionality.
 class AILanguageModelOpenLoopToolTest : public AILanguageModelTest {
+ public:
+  AILanguageModelOpenLoopToolTest() {
+    scoped_feature_list_.InitAndEnableFeature(
+        blink::features::kAIPromptAPIToolUse);
+  }
+
  protected:
   // Override CreateConfig to use higher max tokens for `Tool Use` testing.
   optimization_guide::proto::OnDeviceModelExecutionFeatureConfig CreateConfig()
@@ -1903,6 +1909,8 @@
   void DisableToolCallSimulation() {
     fake_broker_->settings().simulated_tool_calls.clear();
   }
+
+  base::test::ScopedFeatureList scoped_feature_list_;
 };
 
 // Test that tools are embedded in the session's initial context.
@@ -2274,6 +2282,58 @@
   EXPECT_THAT(final_response, testing::HasSubstr("\"condition\":\"Cloudy\""));
 }
 
+// Verify that CanCreate and Create correctly handle the disabled tool use flag.
+TEST_F(AILanguageModelOpenLoopToolTest, RejectCreateWithFlagDisabled) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  scoped_feature_list.InitAndDisableFeature(
+      blink::features::kAIPromptAPIToolUse);
+
+  std::vector<blink::mojom::AILanguageModelToolDeclarationPtr> tools;
+  tools.push_back(CreateWeatherTool());
+  auto options = blink::mojom::AILanguageModelCreateOptions::New();
+  options->tools = std::move(tools);
+
+  base::test::TestFuture<blink::mojom::ModelAvailabilityCheckResult> future;
+  GetAIManagerInterface()->CanCreateLanguageModel(options.Clone(),
+                                                  future.GetCallback());
+  EXPECT_EQ(future.Get(), blink::mojom::ModelAvailabilityCheckResult::
+                              kUnavailableModelAdaptationNotAvailable);
+
+  TestCreateLanguageModelClient client;
+  GetAIManagerRemote()->CreateLanguageModel(client.BindNewPipeAndPassRemote(),
+                                            std::move(options));
+  auto result = client.result().Take();
+  ASSERT_FALSE(result.has_value());
+  EXPECT_EQ(result.error().error,
+            blink::mojom::AIManagerCreateClientError::kUnableToCreateSession);
+}
+
+// Ensure Prompt rejects tool responses on sessions without the tool capability.
+TEST_F(AILanguageModelOpenLoopToolTest, RejectToolResponseWithoutCapability) {
+  auto session = CreateSession();
+  base::DictValue tool_response_dict;
+  tool_response_dict.Set("callID", "c0");
+  tool_response_dict.Set("name", "no_such_tool");
+  base::DictValue result;
+  result.Set("arbitrary_field", "arbitrary_value");
+  tool_response_dict.Set("result", std::move(result));
+
+  std::vector<blink::mojom::AILanguageModelPromptPtr> prompts;
+  auto prompt = blink::mojom::AILanguageModelPrompt::New();
+  prompt->role = Role::kUser;
+  prompt->content.push_back(
+      blink::mojom::AILanguageModelPromptContent::NewToolResponse(
+          std::move(tool_response_dict)));
+  prompts.push_back(std::move(prompt));
+
+  AITestUtils::TestStreamingResponder responder;
+  session->Prompt(std::move(prompts), /*constraint=*/nullptr,
+                  responder.BindRemote());
+  ASSERT_FALSE(responder.WaitForCompletion());
+  EXPECT_EQ(responder.error_status(),
+            blink::mojom::ModelStreamingResponseStatus::kErrorInvalidRequest);
+}
+
 TEST_F(AILanguageModelTest, CanCreatePermissionsPolicyDisabled) {
   DisablePolicy(network::mojom::PermissionsPolicyFeature::kLanguageModel);
   mojo::test::BadMessageObserver observer;
diff --git a/chrome/browser/ai/ai_manager.cc b/chrome/browser/ai/ai_manager.cc
index 7539131..abf25e5 100644
--- a/chrome/browser/ai/ai_manager.cc
+++ b/chrome/browser/ai/ai_manager.cc
@@ -704,9 +704,14 @@
                                   kUnavailableModelAdaptationNotAvailable);
       return;
     }
-    // Note: Tool use capabilities are gated by RuntimeEnabledFeatures in Blink.
     // Tool use capability is signaled by the presence of tool declarations.
     if (options->tools.has_value() && !options->tools->empty()) {
+      // Check if tool declarations are used without the feature flag.
+      if (!base::FeatureList::IsEnabled(blink::features::kAIPromptAPIToolUse)) {
+        std::move(callback).Run(blink::mojom::ModelAvailabilityCheckResult::
+                                    kUnavailableModelAdaptationNotAvailable);
+        return;
+      }
       input_capabilities.Put(on_device_model::CapabilityFlags::kToolUse);
     }
   }
@@ -865,11 +870,13 @@
     return;
   }
   if (!params->capabilities.empty()) {
-    // Check if multimodal input (image/audio) is used without the feature flag
-    // or if the model doesn't support the requested capabilities.
+    // Check if image/audio/tool input types are used without feature flags, or
+    // the model doesn't support the requested capabilities.
     if ((HasMultimodalInputCapabilities(params->capabilities) &&
          !base::FeatureList::IsEnabled(
              blink::features::kAIPromptAPIMultimodalInput)) ||
+        (params->capabilities.Has(on_device_model::CapabilityFlags::kToolUse) &&
+         !base::FeatureList::IsEnabled(blink::features::kAIPromptAPIToolUse)) ||
         !model_client->capabilities().HasAll(params->capabilities)) {
       mojo::Remote<blink::mojom::AIManagerCreateLanguageModelClient>
           client_remote(std::move(client));
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ai/ai_language_model_unittest.cc b/chrome/browser/ai/ai_language_model_unittest.cc
index db0fdf45..3a9f58e 100644
--- a/chrome/browser/ai/ai_language_model_unittest.cc
+++ b/chrome/browser/ai/ai_language_model_unittest.cc
@@ -1827,6 +1827,12 @@
 
 // Test class for `Tool Use` functionality.
 class AILanguageModelOpenLoopToolTest : public AILanguageModelTest {
+ public:
+  AILanguageModelOpenLoopToolTest() {
+    scoped_feature_list_.InitAndEnableFeature(
+        blink::features::kAIPromptAPIToolUse);
+  }
+
  protected:
   // Override CreateConfig to use higher max tokens for `Tool Use` testing.
   optimization_guide::proto::OnDeviceModelExecutionFeatureConfig CreateConfig()
@@ -1903,6 +1909,8 @@
   void DisableToolCallSimulation() {
     fake_broker_->settings().simulated_tool_calls.clear();
   }
+
+  base::test::ScopedFeatureList scoped_feature_list_;
 };
 
 // Test that tools are embedded in the session's initial context.
@@ -2274,6 +2282,58 @@
   EXPECT_THAT(final_response, testing::HasSubstr("\"condition\":\"Cloudy\""));
 }
 
+// Verify that CanCreate and Create correctly handle the disabled tool use flag.
+TEST_F(AILanguageModelOpenLoopToolTest, RejectCreateWithFlagDisabled) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  scoped_feature_list.InitAndDisableFeature(
+      blink::features::kAIPromptAPIToolUse);
+
+  std::vector<blink::mojom::AILanguageModelToolDeclarationPtr> tools;
+  tools.push_back(CreateWeatherTool());
+  auto options = blink::mojom::AILanguageModelCreateOptions::New();
+  options->tools = std::move(tools);
+
+  base::test::TestFuture<blink::mojom::ModelAvailabilityCheckResult> future;
+  GetAIManagerInterface()->CanCreateLanguageModel(options.Clone(),
+                                                  future.GetCallback());
+  EXPECT_EQ(future.Get(), blink::mojom::ModelAvailabilityCheckResult::
+                              kUnavailableModelAdaptationNotAvailable);
+
+  TestCreateLanguageModelClient client;
+  GetAIManagerRemote()->CreateLanguageModel(client.BindNewPipeAndPassRemote(),
+                                            std::move(options));
+  auto result = client.result().Take();
+  ASSERT_FALSE(result.has_value());
+  EXPECT_EQ(result.error().error,
+            blink::mojom::AIManagerCreateClientError::kUnableToCreateSession);
+}
+
+// Ensure Prompt rejects tool responses on sessions without the tool capability.
+TEST_F(AILanguageModelOpenLoopToolTest, RejectToolResponseWithoutCapability) {
+  auto session = CreateSession();
+  base::DictValue tool_response_dict;
+  tool_response_dict.Set("callID", "c0");
+  tool_response_dict.Set("name", "no_such_tool");
+  base::DictValue result;
+  result.Set("arbitrary_field", "arbitrary_value");
+  tool_response_dict.Set("result", std::move(result));
+
+  std::vector<blink::mojom::AILanguageModelPromptPtr> prompts;
+  auto prompt = blink::mojom::AILanguageModelPrompt::New();
+  prompt->role = Role::kUser;
+  prompt->content.push_back(
+      blink::mojom::AILanguageModelPromptContent::NewToolResponse(
+          std::move(tool_response_dict)));
+  prompts.push_back(std::move(prompt));
+
+  AITestUtils::TestStreamingResponder responder;
+  session->Prompt(std::move(prompts), /*constraint=*/nullptr,
+                  responder.BindRemote());
+  ASSERT_FALSE(responder.WaitForCompletion());
+  EXPECT_EQ(responder.error_status(),
+            blink::mojom::ModelStreamingResponseStatus::kErrorInvalidRequest);
+}
+
 TEST_F(AILanguageModelTest, CanCreatePermissionsPolicyDisabled) {
   DisablePolicy(network::mojom::PermissionsPolicyFeature::kLanguageModel);
   mojo::test::BadMessageObserver observer;
Loading diff…

Original Bug Report

reported by [email protected]

Potential missing browser-side enforcement of kAIPromptAPIToolUse flag in AIManager

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 Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: The kAIPromptAPIToolUse feature flag is only enforced in the renderer, potentially allowing a compromised renderer to bypass this gate and access experimental AI tool-use code in the browser and utility processes. This expands the attack surface to include experimental code that was intended to be disabled in production builds.

Affected files:

  • chrome/browser/ai/ai_manager.cc
  • chrome/browser/ai/ai_language_model.cc
  • components/optimization_guide/core/model_execution/model_broker_client.cc
  • services/on_device_model/ml/on_device_model_executor.cc

Estimated timestamp from git blame: 2026-03-10

The Prompt API’s tool-use feature (kAIPromptAPIToolUse) is an experimental capability intended to be gated by a feature flag. However, the browser-side implementation appears to lack sufficient enforcement, potentially allowing a compromised renderer to bypass the flag by sending Mojo IPCs directly to the browser process. This violates the security principle that browser-process IPC handlers must not rely on the renderer to enforce feature gates, especially for experimental features that may not have undergone full security review.

Potential Root Causes

  1. Missing Flag Validation in AIManager: In chrome/browser/ai/ai_manager.cc, the CanCreateLanguageModel and CreateLanguageModelInternal functions do not check the kAIPromptAPIToolUse feature flag when processing tool-use options. The implementation explicitly defers this check to the renderer (see comments at lines 707 and 852), allowing a renderer to request tool-use support even when the flag is disabled.

  2. Unconditional Capability Grant in ModelClient: The capability check in AIManager::CreateLanguageModelInternal is bypassed because ModelClient (in components/optimization_guide/core/model_execution/model_broker_client.cc) unconditionally adds kToolUse to its capability set in its constructor (line 90). Consequently, verification against the model’s capabilities always succeeds for the tool-use bit.

  3. Missing Capability Check for Tool Responses: In chrome/browser/ai/ai_language_model.cc, the ConvertToInput function processes kToolResponse content without verifying if the session has the kToolUse capability. While other experimental input types like images and audio are validated against the session’s capability set, tool responses are accepted and processed unconditionally.

Potential Impact

A compromised renderer could exploit these omissions to reach experimental code paths in both the browser process and the sandboxed On-Device Model Service (ODMS) utility process. This includes:

  • Browser Process: Reaching base::JSONWriter::Write on renderer-supplied dictionary values within AppendToolDeclarations and GetToolResponseFromDict.
  • ODMS Utility Process: Forwarding attacker-controlled tool-related data to the underlying ChromeML library via services/on_device_model/ml/on_device_model_executor.cc, reaching experimental tool-declaration parsers and state machines.

Suggested Reproduction Steps (Potential)

  1. From a compromised renderer (or via MojoJS), bind the blink.mojom.AIManager interface.
  2. Call CreateLanguageModel with options->tools populated. Observe if the browser process creates a session with tool-use capabilities even if the kAIPromptAPIToolUse flag is disabled.
  3. Alternatively, create a standard language model session (without tools) and call Prompt with kToolResponse content. AILanguageModel::ConvertToInput should process the tool response and forward it to the ODMS process without verifying tool-use support.

Suggested Fix

  • In chrome/browser/ai/ai_manager.cc, add explicit checks for base::FeatureList::IsEnabled(blink::features::kAIPromptAPIToolUse) in CanCreateLanguageModel and CreateLanguageModelInternal before allowing tool-related configurations.
  • In components/optimization_guide/core/model_execution/model_broker_client.cc, remove the unconditional addition of kToolUse in the ModelClient constructor and instead derive it from the model’s actual capabilities or the feature flag.
  • In chrome/browser/ai/ai_language_model.cc, add a capability check in ConvertToInput for the kToolResponse tag to ensure the session has the kToolUse capability.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


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