Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in Actor
DescriptionOut of bounds read in Actor
ComponentActor
Sub-componentGlic
Bug ClassOOB
Tracker560157609
Fix commitc34b5740013a (chromium/src) +339/-61
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
components/origin_gating/core/origin_gating_checker.cc
modified

Files Changed

  • components/origin_gating/core/types.cc
  • components/origin_gating/core/origin_gating_checker_unittest.cc
  • chrome/browser/actor/execution_engine_origin_gating_browsertest.cc
  • components/origin_gating/core/origin_gating_checker.cc
  • components/origin_gating/core/types.h
  • chrome/browser/actor/execution_engine.cc
  • components/origin_gating/core/origin_gating_checker.h
From c34b5740013ada22a5f3fbce560bf2fe0ebdda8b Mon Sep 17 00:00:00 2001
From: Chris Fredrickson <cfredric@chromium.org>
Date: Wed, 16 Sep 2026 18:20:00 -0700
Subject: [PATCH] [glic] Don't allow TaskPolicy allows to override static list blocks

This allows glic to treat "block" vs "allow" decisions from
TaskPolicyConfigs differently, so that an "allow" decision doesn't
necessarily override blockages by other predicates.

Behavior change: this makes TaskPolicyConfig usage an almost-purely
restrictive configuration. I.e., supplying an TaskPolicyConfig does
not allow anything that glic couldn't normally allow. (Note that
TaskPolicyConfigs can still bypass the sensitive site prompt for a
given site, if needed.)

Fixed: 560157609
Change-Id: Ifaa74352487beb1d2b9b663a2bd3e3892dd1c3c6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8401268
Auto-Submit: Chris Fredrickson <cfredric@chromium.org>
Reviewed-by: Jeremy Roman <jbroman@chromium.org>
Commit-Queue: Chris Fredrickson <cfredric@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1699401}
---
diff --git a/chrome/browser/actor/execution_engine.cc b/chrome/browser/actor/execution_engine.cc
index e6301b9..4104307 100644
--- a/chrome/browser/actor/execution_engine.cc
+++ b/chrome/browser/actor/execution_engine.cc
@@ -515,10 +515,10 @@
       switch (decision.attribution.Source()) {
         case DecisionSource::kAllowSameOrigin:
           return ExecutionEngine::GatingDecision::kAllowSameOrigin;
-        case DecisionSource::kTaskPolicyConfig:
-          return decision.is_allowed
-                     ? ExecutionEngine::GatingDecision::kAllowByContainerConfig
-                     : ExecutionEngine::GatingDecision::kBlockByContainerConfig;
+        case DecisionSource::kBlockByTaskPolicyConfig:
+          return ExecutionEngine::GatingDecision::kBlockByContainerConfig;
+        case DecisionSource::kAllowByTaskPolicyConfig:
+          return ExecutionEngine::GatingDecision::kAllowByContainerConfig;
         case DecisionSource::kEnterprisePolicy:
           return decision.is_allowed
                      ? ExecutionEngine::GatingDecision::kAllowByStaticList
@@ -578,7 +578,7 @@
           return ProfileIOData::IsHandledURL(url)
                      ? MayActOnUrlBlockReason::kWrongScheme
                      : MayActOnUrlBlockReason::kExternalProtocol;
-        case DecisionSource::kTaskPolicyConfig:
+        case DecisionSource::kBlockByTaskPolicyConfig:
           return MayActOnUrlBlockReason::kBlockedByContainerConfig;
         case DecisionSource::kNoVerdict:
           // `OnNoVerdict` allows navigation requests to proceed, and only
@@ -590,6 +590,7 @@
         case origin_gating::DecisionSource::kAllowAboutBlank:
         case origin_gating::DecisionSource::kCacheWithUserConfirmation:
         case origin_gating::DecisionSource::kCacheWithoutUserConfirmation:
+        case origin_gating::DecisionSource::kAllowByTaskPolicyConfig:
           // Unreachable since these predicates allow the event, but
           // `decision.is_allowed` is false.
           NOTREACHED();
@@ -760,7 +761,7 @@
                                                        task_->GetProfile()),
                                    ActorCustomPredicate::kLookalikeUrl),
                    kRequestsAndPageActions},
-                  {DecisionSource::kTaskPolicyConfig,
+                  {DecisionSource::kBlockByTaskPolicyConfig,
                    {GateableEvent::kNavigationResponse,
                     GateableEvent::kPageAction}},
                   {CreateSafetyListPredicate(),
@@ -776,6 +777,9 @@
                            task_->GetProfile()),
                        ActorCustomPredicate::kSensitiveUrl),
                    {GateableEvent::kNavigationRequest}},
+                  {DecisionSource::kAllowByTaskPolicyConfig,
+                   {GateableEvent::kNavigationResponse,
+                    GateableEvent::kPageAction}},
                   {DecisionSource::kCacheWithoutUserConfirmation,
                    {GateableEvent::kNavigationResponse}},
               },
diff --git a/components/origin_gating/core/origin_gating_checker.cc b/components/origin_gating/core/origin_gating_checker.cc
index efb950b..e05bde6 100644
--- a/components/origin_gating/core/origin_gating_checker.cc
+++ b/components/origin_gating/core/origin_gating_checker.cc
@@ -17,6 +17,7 @@
 #include "base/types/expected_macros.h"
 #include "components/origin_gating/core/origin_gating_cache.h"
 #include "components/origin_gating/core/origin_gating_configuration.h"
+#include "components/origin_gating/core/task_policy_config_slot.h"
 #include "components/origin_gating/core/types.h"
 #include "net/base/url_util.h"
 #include "third_party/abseil-cpp/absl/functional/overload.h"
@@ -111,6 +112,23 @@
   return DecisionAttribution(predicate.attribution());
 }
 
+Decision EvaluateActorContainerConfig(const TaskPolicyConfigSlot& config_slot,
+                                      GateableEvent event,
+                                      const url::Origin& source,
+                                      const url::Origin& destination) {
+  if (!config_slot.has_value()) {
+    return Decision::kNoDecision;
+  }
+  const TaskPolicyConfig& config = config_slot.value();
+  if (event == GateableEvent::kPageAction) {
+    return config.IsActuationAllowed(destination) ? Decision::kAllowed
+                                                  : Decision::kBlocked;
+  }
+
+  return config.IsNavigationAllowed(source, destination) ? Decision::kAllowed
+                                                         : Decision::kBlocked;
+}
+
 }  // namespace
 
 OriginGatingChecker::OriginGatingChecker(base::WeakPtr<Delegate> delegate,
@@ -245,9 +263,14 @@
       return EvaluateRequireHttpsOrLocalhost(input.destination);
     case DecisionSource::kRequireHttpsOrHttp:
       return EvaluateRequireHttpsOrHttp(input.destination);
-    case DecisionSource::kTaskPolicyConfig:
-      return EvaluateTaskPolicyConfig(input.event, input.source_origin,
-                                      input.destination_origin);
+    case DecisionSource::kBlockByTaskPolicyConfig:
+      return EvaluateTaskPolicyConfigWithCache(input) == Decision::kBlocked
+                 ? Decision::kBlocked
+                 : Decision::kNoDecision;
+    case DecisionSource::kAllowByTaskPolicyConfig:
+      return EvaluateTaskPolicyConfigWithCache(input) == Decision::kAllowed
+                 ? Decision::kAllowed
+                 : Decision::kNoDecision;
     case DecisionSource::kNoVerdict:
       // This is an internal/fallback decision source and is not an
       // executable predicate. OriginGatingConfiguration's
@@ -386,21 +409,14 @@
                                                     : Decision::kNoDecision;
 }
 
-Decision OriginGatingChecker::EvaluateTaskPolicyConfig(
-    GateableEvent event,
-    const url::Origin& source,
-    const url::Origin& destination) const {
-  if (!task_policy_config_slot_.has_value()) {
-    return Decision::kNoDecision;
+Decision OriginGatingChecker::EvaluateTaskPolicyConfigWithCache(
+    DelegateInputs& input) const {
+  if (!input.actor_container_decision.has_value()) {
+    input.actor_container_decision = EvaluateActorContainerConfig(
+        task_policy_config_slot_, input.event, input.source_origin,
+        input.destination_origin);
   }
-  const TaskPolicyConfig& config = task_policy_config_slot_.value();
-  if (event == GateableEvent::kPageAction) {
-    return config.IsActuationAllowed(destination) ? Decision::kAllowed
-                                                  : Decision::kBlocked;
-  }
-
-  return config.IsNavigationAllowed(source, destination) ? Decision::kAllowed
-                                                         : Decision::kBlocked;
+  return input.actor_container_decision.value();
 }
 
 }  // namespace origin_gating
diff --git a/components/origin_gating/core/origin_gating_checker.h b/components/origin_gating/core/origin_gating_checker.h
index 724dd2c..2a3c16b 100644
--- a/components/origin_gating/core/origin_gating_checker.h
+++ b/components/origin_gating/core/origin_gating_checker.h
@@ -129,6 +129,10 @@
     GURL destination;
     url::Origin destination_origin;
     std::optional<bool> requires_user_confirmation;
+    // The decision from the ActorContainerConfig, or `kNoDecision` if there is
+    // no config. This is `std::nullopt` until a predicate that involves the
+    // ActorContainerConfig is consulted.
+    std::optional<Decision> actor_container_decision;
   };
 
   void EvaluatePredicates(
@@ -201,9 +205,7 @@
   Decision IsCachedWithUserConfirmation(const url::Origin& origin) const
       VALID_CONTEXT_REQUIRED(sequence_checker_);
 
-  Decision EvaluateTaskPolicyConfig(GateableEvent event,
-                                    const url::Origin& source,
-                                    const url::Origin& destination) const
+  Decision EvaluateTaskPolicyConfigWithCache(DelegateInputs& input) const
       VALID_CONTEXT_REQUIRED(sequence_checker_);
 
   SEQUENCE_CHECKER(sequence_checker_);
diff --git a/components/origin_gating/core/types.cc b/components/origin_gating/core/types.cc
index b555f84..5e6a94f7 100644
--- a/components/origin_gating/core/types.cc
+++ b/components/origin_gating/core/types.cc
@@ -43,8 +43,10 @@
       return "RequireHttpsOrLocalhost";
     case DecisionSource::kRequireHttpsOrHttp:
       return "RequireHttpsOrHttp";
-    case DecisionSource::kTaskPolicyConfig:
-      return "TaskPolicyConfig";
+    case DecisionSource::kBlockByTaskPolicyConfig:
+      return "BlockByTaskPolicyConfig";
+    case DecisionSource::kAllowByTaskPolicyConfig:
+      return "AllowByTaskPolicyConfig";
     case DecisionSource::kNoVerdict:
       return "NoVerdict";
   }
diff --git a/components/origin_gating/core/types.h b/components/origin_gating/core/types.h
index fb05424..7e2fdd12 100644
--- a/components/origin_gating/core/types.h
+++ b/components/origin_gating/core/types.h
@@ -78,8 +78,11 @@
   // http.
   kRequireHttpsOrHttp,
   // Predicate that evaluates the destination against the task policy
-  // configuration.
-  kTaskPolicyConfig,
+  // configuration and blocks if blocked, otherwise returns kNoDecision.
+  kBlockByTaskPolicyConfig,
+  // Predicate that evaluates the destination against the task policy
+  // configuration and allows if allowed, otherwise returns kNoDecision.
+  kAllowByTaskPolicyConfig,
   // No decision was reached before the OriginGating framework ran out of
   // predicates to run.
   kNoVerdict,
diff --git a/chrome/browser/actor/execution_engine_origin_gating_browsertest.cc b/chrome/browser/actor/execution_engine_origin_gating_browsertest.cc
index de071ba..872bb03 100644
--- a/chrome/browser/actor/execution_engine_origin_gating_browsertest.cc
+++ b/chrome/browser/actor/execution_engine_origin_gating_browsertest.cc
@@ -33,6 +33,7 @@
 #include "components/optimization_guide/core/filters/hints_component_util.h"
 #include "components/optimization_guide/core/filters/optimization_hints_component_update_listener.h"
 #include "components/optimization_guide/proto/features/common_quality_data.pb.h"
+#include "components/optimization_guide/proto/hints.pb.h"
 #include "components/ukm/test_ukm_recorder.h"
 #include "content/public/test/back_forward_cache_util.h"
 #include "content/public/test/browser_test.h"
@@ -142,6 +143,23 @@
 constexpr std::string_view kSameSiteInitiatorHistogram =
     "Actor.NavigationGating.SameSiteInitiator";
 
+optimization_guide::proto::LocationRule* AddAllowlistedSiteToActorConfig(
+    std::string_view domain,
+    optimization_guide::proto::AgentContainerConfig& config_proto) {
+  optimization_guide::proto::LocationRule* rule =
+      config_proto.add_location_rules();
+  optimization_guide::proto::Site* site =
+      rule->mutable_location()->mutable_site();
+  site->set_protocol(optimization_guide::proto::Protocol::PROTOCOL_HTTPS);
+  site->set_domain(domain);
+  optimization_guide::proto::RuleMetadata* metadata = rule->mutable_metadata();
+  metadata->add_capabilities(
+      optimization_guide::proto::RuleMetadata::CAPABILITY_ALL);
+  metadata->add_accessible_resources(
+      optimization_guide::proto::RuleMetadata::RESOURCE_SESSION);
+  return rule;
+}
+
 }  // namespace
 
 // TODO(crbug.com/537849016): Simplify this test suite to GlicBrowserTest.
@@ -1743,17 +1761,7 @@
 IN_PROC_BROWSER_TEST_F(ExecutionEngineOriginGatingBrowserTest,
                        TaskPolicyConfig_Navigation) {
   optimization_guide::proto::AgentContainerConfig config_proto;
-  optimization_guide::proto::LocationRule* rule =
-      config_proto.add_location_rules();
-  optimization_guide::proto::Site* site =
-      rule->mutable_location()->mutable_site();
-  site->set_protocol(optimization_guide::proto::Protocol::PROTOCOL_HTTPS);
-  site->set_domain("example.com");
-  optimization_guide::proto::RuleMetadata* metadata = rule->mutable_metadata();
-  metadata->add_capabilities(
-      optimization_guide::proto::RuleMetadata::CAPABILITY_ALL);
-  metadata->add_accessible_resources(
-      optimization_guide::proto::RuleMetadata::RESOURCE_SESSION);
+  AddAllowlistedSiteToActorConfig("example.com", config_proto);
 
   const GURL allowed_url = embedded_https_test_server().GetURL(
       "foo.example.com", "/actor/blank.html");
@@ -1796,20 +1804,12 @@
                        TaskPolicyConfig_TaskStart) {
   optimization_guide::proto::AgentContainerConfig config_proto;
   optimization_guide::proto::LocationRule* rule =
-      config_proto.add_location_rules();
-  optimization_guide::proto::Site* site =
-      rule->mutable_location()->mutable_site();
-  site->set_protocol(optimization_guide::proto::Protocol::PROTOCOL_HTTPS);
-  site->set_domain("example.com");
-  optimization_guide::proto::RuleMetadata* metadata = rule->mutable_metadata();
-  metadata->add_capabilities(
-      optimization_guide::proto::RuleMetadata::CAPABILITY_ALL);
-  metadata->add_accessible_resources(
-      optimization_guide::proto::RuleMetadata::RESOURCE_SESSION);
+      AddAllowlistedSiteToActorConfig("example.com", config_proto);
   // Add a NavigationSource that should be ignored.
   optimization_guide::proto::NavigationSource* nav_source =
       rule->add_navigation_sources();
-  site = nav_source->mutable_source()->mutable_site();
+  optimization_guide::proto::Site* site =
+      nav_source->mutable_source()->mutable_site();
   site->set_protocol(optimization_guide::proto::Protocol::PROTOCOL_HTTPS);
   site->set_domain("foo.com");
 
@@ -1831,6 +1831,157 @@
   ExpectErrorResult(result, mojom::ActionResultCode::kUrlBlocked);
 }
 
+// If the actor container config would allow a navigation but some other
+// predicate would disallow it, it should be disallowed.
+IN_PROC_BROWSER_TEST_F(
+    ExecutionEngineOriginGatingBrowserTest,
+    ActorContainerConfig_SafetyListBlockOverridesContainerAllow_Navigation) {
+  base::HistogramTester histogram_tester;
+  const GURL destination_url =
+      embedded_https_test_server().GetURL("bar.com", "/actor/blank.html");
+  const GURL start_page_url = embedded_https_test_server().GetURL(
+      "foo.com",
+      base::StrCat({"/actor/link_full_page.html?href=",
+                    url::EncodeUriComponent(destination_url.spec())}));
+
+  // Block the destination URL in SafetyListManager.
+  ParseSafetyListsForTesting(SafetyListManager::GetInstance(), R"json(
+    {
+      "navigation_blocked": [
+        { "from": "*", "to": "bar.com" }
+      ]
+    }
+  )json");
+
+  // AgentContainerConfig allows both foo.com (to actuate the link) and
+  // bar.com (the navigation destination).
+  optimization_guide::proto::AgentContainerConfig config_proto;
+  AddAllowlistedSiteToActorConfig("foo.com", config_proto);
+  AddAllowlistedSiteToActorConfig("bar.com", config_proto);
+
+  ASSERT_TRUE(content::NavigateToURL(web_contents(), start_page_url));
+  OpenGlicAndCreateTask();
+
+  std::unique_ptr<ToolRequest> click_link =
+      MakeClickRequest(*active_tab(), gfx::Point(1, 1));
+
+  PerformActionsFuture result;
+  actor_keyed_service().PerformActions(
+      actor_task().id(), ToRequestList(click_link),
+      ActorTaskMetadata::WithAgentContainerConfigForTesting(config_proto),
+      result.GetCallback());
+  ExpectErrorResult(result,
+                    mojom::ActionResultCode::kActionsBlockedForSiteRisk);
+
+  histogram_tester.ExpectUniqueSample(
+      "Actor.NavigationGating.GatingDecision2",
+      ExecutionEngine::GatingDecision::kBlockByStaticList, 1);
+}
+
+// If the actor container config would allow an action on the page but some
+// other predicate would disallow it, it should be disallowed.
+IN_PROC_BROWSER_TEST_F(
+    ExecutionEngineOriginGatingBrowserTest,
+    ActorContainerConfig_SafetyListBlockOverridesContainerAllow_PageAction) {
+  const GURL page_url =
+      embedded_https_test_server().GetURL("example.com", "/actor/blank.html");
+
+  // Block the page URL in SafetyListManager.
+  ParseSafetyListsForTesting(SafetyListManager::GetInstance(), R"json(
+    {
+      "navigation_blocked": [
+        { "from": "*", "to": "example.com" }
+      ]
+    }
+  )json");
+
+  // AgentContainerConfig allows example.com.
+  optimization_guide::proto::AgentContainerConfig config_proto;
+  AddAllowlistedSiteToActorConfig("example.com", config_proto);
+
+  ASSERT_TRUE(content::NavigateToURL(web_contents(), page_url));
+  OpenGlicAndCreateTask();
+
+  std::unique_ptr<ToolRequest> click =
+      MakeClickRequest(*active_tab(), gfx::Point(1, 1));
+
+  // Should not be able to actuate the blocked page even though
+  // AgentContainerConfig allows it.
+  PerformActionsFuture result;
+  actor_keyed_service().PerformActions(
+      actor_task().id(), ToRequestList(click),
+      ActorTaskMetadata::WithAgentContainerConfigForTesting(config_proto),
+      result.GetCallback());
+  ExpectErrorResult(result,
+                    mojom::ActionResultCode::kActionsBlockedForSiteRisk);
+}
+
+IN_PROC_BROWSER_TEST_F(ExecutionEngineOriginGatingBrowserTest,
+                       ActorContainerConfig_NavigationAllowed) {
+  base::HistogramTester histogram_tester;
+  const GURL destination_url =
+      embedded_https_test_server().GetURL("bar.com", "/actor/blank.html");
+  const GURL start_page_url = embedded_https_test_server().GetURL(
+      "foo.com",
+      base::StrCat({"/actor/link_full_page.html?href=",
+                    url::EncodeUriComponent(destination_url.spec())}));
+
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/actor/execution_engine_origin_gating_browsertest.cc b/chrome/browser/actor/execution_engine_origin_gating_browsertest.cc
index de071ba..872bb03 100644
--- a/chrome/browser/actor/execution_engine_origin_gating_browsertest.cc
+++ b/chrome/browser/actor/execution_engine_origin_gating_browsertest.cc
@@ -33,6 +33,7 @@
 #include "components/optimization_guide/core/filters/hints_component_util.h"
 #include "components/optimization_guide/core/filters/optimization_hints_component_update_listener.h"
 #include "components/optimization_guide/proto/features/common_quality_data.pb.h"
+#include "components/optimization_guide/proto/hints.pb.h"
 #include "components/ukm/test_ukm_recorder.h"
 #include "content/public/test/back_forward_cache_util.h"
 #include "content/public/test/browser_test.h"
@@ -142,6 +143,23 @@
 constexpr std::string_view kSameSiteInitiatorHistogram =
     "Actor.NavigationGating.SameSiteInitiator";
 
+optimization_guide::proto::LocationRule* AddAllowlistedSiteToActorConfig(
+    std::string_view domain,
+    optimization_guide::proto::AgentContainerConfig& config_proto) {
+  optimization_guide::proto::LocationRule* rule =
+      config_proto.add_location_rules();
+  optimization_guide::proto::Site* site =
+      rule->mutable_location()->mutable_site();
+  site->set_protocol(optimization_guide::proto::Protocol::PROTOCOL_HTTPS);
+  site->set_domain(domain);
+  optimization_guide::proto::RuleMetadata* metadata = rule->mutable_metadata();
+  metadata->add_capabilities(
+      optimization_guide::proto::RuleMetadata::CAPABILITY_ALL);
+  metadata->add_accessible_resources(
+      optimization_guide::proto::RuleMetadata::RESOURCE_SESSION);
+  return rule;
+}
+
 }  // namespace
 
 // TODO(crbug.com/537849016): Simplify this test suite to GlicBrowserTest.
@@ -1743,17 +1761,7 @@
 IN_PROC_BROWSER_TEST_F(ExecutionEngineOriginGatingBrowserTest,
                        TaskPolicyConfig_Navigation) {
   optimization_guide::proto::AgentContainerConfig config_proto;
-  optimization_guide::proto::LocationRule* rule =
-      config_proto.add_location_rules();
-  optimization_guide::proto::Site* site =
-      rule->mutable_location()->mutable_site();
-  site->set_protocol(optimization_guide::proto::Protocol::PROTOCOL_HTTPS);
-  site->set_domain("example.com");
-  optimization_guide::proto::RuleMetadata* metadata = rule->mutable_metadata();
-  metadata->add_capabilities(
-      optimization_guide::proto::RuleMetadata::CAPABILITY_ALL);
-  metadata->add_accessible_resources(
-      optimization_guide::proto::RuleMetadata::RESOURCE_SESSION);
+  AddAllowlistedSiteToActorConfig("example.com", config_proto);
 
   const GURL allowed_url = embedded_https_test_server().GetURL(
       "foo.example.com", "/actor/blank.html");
@@ -1796,20 +1804,12 @@
                        TaskPolicyConfig_TaskStart) {
   optimization_guide::proto::AgentContainerConfig config_proto;
   optimization_guide::proto::LocationRule* rule =
-      config_proto.add_location_rules();
-  optimization_guide::proto::Site* site =
-      rule->mutable_location()->mutable_site();
-  site->set_protocol(optimization_guide::proto::Protocol::PROTOCOL_HTTPS);
-  site->set_domain("example.com");
-  optimization_guide::proto::RuleMetadata* metadata = rule->mutable_metadata();
-  metadata->add_capabilities(
-      optimization_guide::proto::RuleMetadata::CAPABILITY_ALL);
-  metadata->add_accessible_resources(
-      optimization_guide::proto::RuleMetadata::RESOURCE_SESSION);
+      AddAllowlistedSiteToActorConfig("example.com", config_proto);
   // Add a NavigationSource that should be ignored.
   optimization_guide::proto::NavigationSource* nav_source =
       rule->add_navigation_sources();
-  site = nav_source->mutable_source()->mutable_site();
+  optimization_guide::proto::Site* site =
+      nav_source->mutable_source()->mutable_site();
   site->set_protocol(optimization_guide::proto::Protocol::PROTOCOL_HTTPS);
   site->set_domain("foo.com");
 
@@ -1831,6 +1831,157 @@
   ExpectErrorResult(result, mojom::ActionResultCode::kUrlBlocked);
 }
 
+// If the actor container config would allow a navigation but some other
+// predicate would disallow it, it should be disallowed.
+IN_PROC_BROWSER_TEST_F(
+    ExecutionEngineOriginGatingBrowserTest,
+    ActorContainerConfig_SafetyListBlockOverridesContainerAllow_Navigation) {
+  base::HistogramTester histogram_tester;
+  const GURL destination_url =
+      embedded_https_test_server().GetURL("bar.com", "/actor/blank.html");
+  const GURL start_page_url = embedded_https_test_server().GetURL(
+      "foo.com",
+      base::StrCat({"/actor/link_full_page.html?href=",
+                    url::EncodeUriComponent(destination_url.spec())}));
+
+  // Block the destination URL in SafetyListManager.
+  ParseSafetyListsForTesting(SafetyListManager::GetInstance(), R"json(
+    {
+      "navigation_blocked": [
+        { "from": "*", "to": "bar.com" }
+      ]
+    }
+  )json");
+
+  // AgentContainerConfig allows both foo.com (to actuate the link) and
+  // bar.com (the navigation destination).
+  optimization_guide::proto::AgentContainerConfig config_proto;
+  AddAllowlistedSiteToActorConfig("foo.com", config_proto);
+  AddAllowlistedSiteToActorConfig("bar.com", config_proto);
+
+  ASSERT_TRUE(content::NavigateToURL(web_contents(), start_page_url));
+  OpenGlicAndCreateTask();
+
+  std::unique_ptr<ToolRequest> click_link =
+      MakeClickRequest(*active_tab(), gfx::Point(1, 1));
+
+  PerformActionsFuture result;
+  actor_keyed_service().PerformActions(
+      actor_task().id(), ToRequestList(click_link),
+      ActorTaskMetadata::WithAgentContainerConfigForTesting(config_proto),
+      result.GetCallback());
+  ExpectErrorResult(result,
+                    mojom::ActionResultCode::kActionsBlockedForSiteRisk);
+
+  histogram_tester.ExpectUniqueSample(
+      "Actor.NavigationGating.GatingDecision2",
+      ExecutionEngine::GatingDecision::kBlockByStaticList, 1);
+}
+
+// If the actor container config would allow an action on the page but some
+// other predicate would disallow it, it should be disallowed.
+IN_PROC_BROWSER_TEST_F(
+    ExecutionEngineOriginGatingBrowserTest,
+    ActorContainerConfig_SafetyListBlockOverridesContainerAllow_PageAction) {
+  const GURL page_url =
+      embedded_https_test_server().GetURL("example.com", "/actor/blank.html");
+
+  // Block the page URL in SafetyListManager.
+  ParseSafetyListsForTesting(SafetyListManager::GetInstance(), R"json(
+    {
+      "navigation_blocked": [
+        { "from": "*", "to": "example.com" }
+      ]
+    }
+  )json");
+
+  // AgentContainerConfig allows example.com.
+  optimization_guide::proto::AgentContainerConfig config_proto;
+  AddAllowlistedSiteToActorConfig("example.com", config_proto);
+
+  ASSERT_TRUE(content::NavigateToURL(web_contents(), page_url));
+  OpenGlicAndCreateTask();
+
+  std::unique_ptr<ToolRequest> click =
+      MakeClickRequest(*active_tab(), gfx::Point(1, 1));
+
+  // Should not be able to actuate the blocked page even though
+  // AgentContainerConfig allows it.
+  PerformActionsFuture result;
+  actor_keyed_service().PerformActions(
+      actor_task().id(), ToRequestList(click),
+      ActorTaskMetadata::WithAgentContainerConfigForTesting(config_proto),
+      result.GetCallback());
+  ExpectErrorResult(result,
+                    mojom::ActionResultCode::kActionsBlockedForSiteRisk);
+}
+
+IN_PROC_BROWSER_TEST_F(ExecutionEngineOriginGatingBrowserTest,
+                       ActorContainerConfig_NavigationAllowed) {
+  base::HistogramTester histogram_tester;
+  const GURL destination_url =
+      embedded_https_test_server().GetURL("bar.com", "/actor/blank.html");
+  const GURL start_page_url = embedded_https_test_server().GetURL(
+      "foo.com",
+      base::StrCat({"/actor/link_full_page.html?href=",
+                    url::EncodeUriComponent(destination_url.spec())}));
+
+  // AgentContainerConfig allows both foo.com (to actuate the link) and
+  // bar.com (the navigation destination).
+  optimization_guide::proto::AgentContainerConfig config_proto;
+  AddAllowlistedSiteToActorConfig("foo.com", config_proto);
+  AddAllowlistedSiteToActorConfig("bar.com", config_proto);
+
+  ASSERT_TRUE(content::NavigateToURL(web_contents(), start_page_url));
+  OpenGlicAndCreateTask();
+
+  PerformActionsFuture result;
+  actor_keyed_service().PerformActions(
+      actor_task().id(),
+      ToRequestList(MakeClickRequest(*active_tab(), gfx::Point(1, 1))),
+      ActorTaskMetadata::WithAgentContainerConfigForTesting(config_proto),
+      result.GetCallback());
+  ExpectOkResult(result);
+
+  histogram_tester.ExpectUniqueSample(
+      "Actor.NavigationGating.GatingDecision2",
+      ExecutionEngine::GatingDecision::kAllowByContainerConfig, 1);
+}
+
+IN_PROC_BROWSER_TEST_F(ExecutionEngineOriginGatingBrowserTest,
+                       ActorContainerConfig_NavigationAllowed_SensitiveSite) {
+  base::HistogramTester histogram_tester;
+  const GURL destination_url = embedded_https_test_server().GetURL(
+      "sensitive.example.com", "/actor/blank.html");
+  const GURL start_page_url = embedded_https_test_server().GetURL(
+      "foo.com",
+      base::StrCat({"/actor/link_full_page.html?href=",
+                    url::EncodeUriComponent(destination_url.spec())}));
+
+  // AgentContainerConfig allows both foo.com (to actuate the link) and
+  // example.com (the navigation destination).
+  optimization_guide::proto::AgentContainerConfig config_proto;
+  AddAllowlistedSiteToActorConfig("foo.com", config_proto);
+  AddAllowlistedSiteToActorConfig("example.com", config_proto);
+
+  ASSERT_TRUE(content::NavigateToURL(web_contents(), start_page_url));
+  OpenGlicAndCreateTask();
+
+  PerformActionsFuture result;
+  actor_keyed_service().PerformActions(
+      actor_task().id(),
+      ToRequestList(MakeClickRequest(*active_tab(), gfx::Point(1, 1))),
+      ActorTaskMetadata::WithAgentContainerConfigForTesting(config_proto),
+      result.GetCallback());
+  // Note that the navigation succeeds, even though we did not set up a
+  // confirmation handler.
+  ExpectOkResult(result);
+
+  histogram_tester.ExpectUniqueSample(
+      "Actor.NavigationGating.GatingDecision2",
+      ExecutionEngine::GatingDecision::kAllowByContainerConfig, 1);
+}
+
 class ExecutionEngineOriginGatingParamBrowserTest
     : public ExecutionEngineOriginGatingBrowserTestBase,
       public testing::WithParamInterface<std::tuple<
diff --git a/components/origin_gating/core/origin_gating_checker_unittest.cc b/components/origin_gating/core/origin_gating_checker_unittest.cc
index 1d707d49..7546784 100644
--- a/components/origin_gating/core/origin_gating_checker_unittest.cc
+++ b/components/origin_gating/core/origin_gating_checker_unittest.cc
@@ -10,6 +10,7 @@
 
 #include "base/functional/callback.h"
 #include "base/run_loop.h"
+#include "base/test/bind.h"
 #include "base/test/gmock_callback_support.h"
 #include "base/test/gtest_util.h"
 #include "base/test/task_environment.h"
@@ -45,6 +46,14 @@
   kOtherCustom2,
 };
 
+TaskPolicyConfig::Rule AllowlistedRule() {
+  return TaskPolicyConfig::Rule(
+      /*navigation_sources=*/{},
+      /*resources=*/{TaskPolicyConfig::Rule::Resource::kSession},
+      /*capabilities=*/
+      {TaskPolicyConfig::Rule::Capability::kAll});
+}
+
 }  // namespace
 
 template <>
@@ -705,7 +714,7 @@
   OriginGatingChecker checker(
       delegate_.GetWeakPtr(),
       OriginGatingConfiguration(
-          {{DecisionSource::kTaskPolicyConfig, GateableEventSet::All()}},
+          {{DecisionSource::kBlockByTaskPolicyConfig, GateableEventSet::All()}},
           /*use_site_keyed_cache=*/false));
 
   GURL source("https://example.com");
@@ -728,7 +737,7 @@
   OriginGatingChecker checker(
       delegate_.GetWeakPtr(),
       OriginGatingConfiguration(
-          {{DecisionSource::kTaskPolicyConfig, GateableEventSet::All()}},
+          {{DecisionSource::kAllowByTaskPolicyConfig, GateableEventSet::All()}},
           /*use_site_keyed_cache=*/false));
 
   // Configure the slot on the checker.
@@ -753,7 +762,7 @@
       checker, nullptr, source, destination);
 
   EXPECT_TRUE(decision.is_allowed);
-  EXPECT_EQ(decision.attribution, DecisionSource::kTaskPolicyConfig);
+  EXPECT_EQ(decision.attribution, DecisionSource::kAllowByTaskPolicyConfig);
 }
 
 TEST_F(OriginGatingCheckerTest,
@@ -761,7 +770,7 @@
   OriginGatingChecker checker(
       delegate_.GetWeakPtr(),
       OriginGatingConfiguration(
-          {{DecisionSource::kTaskPolicyConfig, GateableEventSet::All()}},
+          {{DecisionSource::kBlockByTaskPolicyConfig, GateableEventSet::All()}},
           /*use_site_keyed_cache=*/false));
 
   // Set an empty config which blocks all navigations.
... (truncated)
Loading diff…

Original Bug Report

reported by vm...@google.com

OriginGating bypass via untrusted agent_container_config in Glic Actor

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: The browser process installs a guest-supplied security policy proto (SecurityMetadata.agent_container_config, carried in the Actions message from the Glic guest webview) directly into the actor task’s OriginGatingChecker with no authenticity or semantic validation. Because that predicate is evaluated ahead of the component-updater Actor Safety Lists block list and the sensitive-origin user-confirmation fallback, and returns a definitive allow/block, one forged wildcard rule turns every subsequent page-action and navigation-commit gate into an unconditional allow. A compromised Glic guest (Gemini web client renderer, or XSS on that origin) can then drive the actor to act on and navigate to GLIC_ACTION_PAGE_BLOCK / block-listed sensitive sites (banking, health) with no block and no user consent dialog. Demonstrated end-to-end by an executed proof of concept against a stock build.

Affected files:

  • chrome/browser/actor/actor_keyed_service.cc:778-797 — unauthenticated, first-wins assignment of the guest config into the origin-gating checker
  • chrome/browser/actor/actor_task_metadata.cc:42-45 — verbatim copy of security().agent_container_config() from guest-supplied Actions bytes
  • chrome/browser/glic/actor/glic_actor_task_manager.cc:434-512 — browser-side handler; validates only task_id, forwards metadata untouched
  • chrome/browser/actor/actor_proto_conversion.cc:1609-1635 — purely syntactic conversion (wildcard at :1055-1057, CAPABILITY_ALL/RESOURCE_SESSION at :1084-1105)
  • chrome/browser/actor/execution_engine.cc:757-763 — kActorContainerConfig predicate ordered before the SafetyList predicate and confirmation cache
  • components/origin_gating/core/origin_gating_checker.cc:379-394 — populated slot yields a definitive decision, halting predicate evaluation
  • components/origin_gating/core/actor_container_config_slot.cc:17-22 — strictly first-wins slot; the guest-fed path is its only production assigner
  • components/origin_gating/core/actor_container_config.cc:73-189 — wildcard location matches any URL; empty sources match any source; CAPABILITY_ALL+RESOURCE_SESSION allow all navigation and actuation

Estimated timestamp from git blame: 2026-05-11

Summary

optimization_guide::proto::Actions bytes sent by the Glic guest are relayed verbatim from the chrome://glic WebUI to the browser (ActorHandler::PerformActions). The handler validates only task_id; the embedded SecurityMetadata.agent_container_config — designed as a server-authored restriction on the agent — is parsed, converted syntactically, and assigned first-wins into the task’s OriginGatingChecker with no authenticity check. Because the bytes transit the untrusted guest, the browser cannot distinguish a server restriction from a guest-forged grant.

A minimal forged config — one rule with location: {wildcard: {}}, accessible_resources: [RESOURCE_SESSION], capabilities: [CAPABILITY_ALL] — makes EvaluateActorContainerConfig return a definitive kAllowed for every gated event. Predicate evaluation stops there, so three later layers are never consulted:

  • the component-updater Actor Safety Lists block list (block entries enforced by default via kGlicEnforceComponentUpdaterBlockListEntries);
  • the sensitive-origin user-confirmation dialog (for_sensitive_origin=true, driven by the GLIC_ACTION_PAGE_BLOCK optimization-guide hint), where user denial would block the action;
  • the new-origin confirmation flow in the OnNoVerdict fallback.

The origin-gating checker is the sole consumer of SafetyListManager::Find and of the sensitive-URL check in the actor path — there is no backstop. Predicates ordered earlier (error-document, SafeBrowsing, https-requirement, IP-literal, dangerous-MIME, enterprise policy, lookalike) still apply, so the bypass is precisely the safety-list + user-consent layer that exists to constrain a hostile agent. All involved features (kGlic on Win/Mac/CrOS/Android, kGlicActor, kGlicCrossOriginNavigationGating) are enabled by default; no flag gates the assignment. Preconditions: a Glic-eligible signed-in profile with completed FRE, and attacker code in the Glic guest (compromised guest renderer or XSS on the Gemini client origin) — the guest hosts remote HTTPS content, not WebUI.


Root Cause Analysis

  1. Guest bytes reach the browser with only task_id validated: chrome/browser/glic/actor/glic_actor_task_manager.cc:434-512 — the guest’s performActions ArrayBuffer arrives via the WebUI relay (chrome/browser/resources/glic/glic_api_impl/actor/actor_host.ts:63-81 → ActorHandler::PerformActions, chrome/browser/glic/host/glic.mojom:933-934); the handler parses the proto, checks only ValidateTaskIdMatchesCurrent, and forwards actor::ActorTaskMetadata(actions) verbatim at :507.
  2. Verbatim parse of the security policy field: chrome/browser/actor/actor_task_metadata.cc:42-45 — copies task_metadata.security().agent_container_config() unmodified.
  3. Unauthenticated first-wins install into the gating checker: chrome/browser/actor/actor_keyed_service.cc:778-797:
    // chrome/browser/actor/actor_keyed_service.cc:778-791
    if (task_metadata.agent_container_config().has_value()) {
      ...
      if (!task->GetExecutionEngine().origin_gating_checker()
               .actor_container_config_slot().has_value()) {
        origin_gating::ActorContainerConfig config = ConvertAgentContainerConfig(
            task_metadata.agent_container_config().value());  // <-- purely syntactic conversion
        ...
        task->GetExecutionEngine().origin_gating_checker()
            .actor_container_config_slot().Assign(std::move(config));  // <-- no authenticity/allowlist check
    
    ActorContainerConfigSlot::Assign (components/origin_gating/core/actor_container_config_slot.cc:17-22) is strictly first-wins, and this guest-fed path is its only production caller — no trusted browser-side source ever populates the slot, so the forged config always wins.
  4. A wildcard rule grants everything: components/origin_gating/core/actor_container_config.cc:73-189 — Location::Matches is unconditionally true for Wildcard (:73-82); empty navigation_sources matches any source (:118-124); CanNavigate() is true with kAll+kSession (:126-129); both IsActuationAllowed and IsNavigationAllowed fall through to the wildcard rule (:153-189).
  5. Definitive decision short-circuits the safety layer: components/origin_gating/core/origin_gating_checker.cc:379-394 — with the slot populated, the predicate never returns kNoDecision, and ProcessDecision(kAllowed) resolves the gate and stops evaluation (:81-104, :172-174). In chrome/browser/actor/execution_engine.cc:757-763, kActorContainerConfig precedes CreateSafetyListPredicate() and kCacheWithUserConfirmation, so the SafetyList (components/actor/core/safety_list_manager.cc:123-150) and the sensitive-origin consent fallback (execution_engine.cc:999-1044, dialog at :1037-1043, denial blocks at :1164-1183) are unreachable for both per-action gating (:1360-1403) and navigation-commit gating via ActorNavigationThrottle (:850-889).

Reproduction

  1. On a Glic-eligible profile (signed-in eligible account, FRE completed; kGlic/kGlicActor/kGlicCrossOriginNavigationGating at their enabled defaults), run attacker JavaScript in the Glic guest webview (compromised guest renderer, or XSS on the Gemini client origin).
  2. Perform the normal actor bootstrap (createTask, createActorTab) to obtain a live task_id.
  3. Send a performActions message whose serialized Actions proto carries the valid task_id, any first action, and task_metadata.security.agent_container_config.location_rules = [{location: {wildcard: {}}, metadata: {accessible_resources: [RESOURCE_SESSION], capabilities: [CAPABILITY_ALL]}}].
  4. Send further performActions with NavigateAction to an Actor-Safety-Lists block-listed origin and to a sensitive (GLIC_ACTION_PAGE_BLOCK-class) origin, followed by ClickAction/TypeAction on those tabs.
  5. Expected: the block-listed navigation fails with kActionsBlockedForSiteRisk (SafetyList block), and the sensitive-origin action raises the for_sensitive_origin user-confirmation dialog, whose denial blocks with kTriggeredNavigationBlocked.

Observed (executed POC on a stock build; control vs. attack rounds on identical browser configuration, differing only in the forged field): the browser journal logs ActorContainerConfigSlot::Assign — status: assigned with the wildcard config, all four gating decisions resolve allowed / ActorContainerConfig, navigations to and clicks on both the block-listed and the sensitive origin return kOk with the target pages registering the actuation, and zero consent dialogs are shown. The control round produces blocked / actor_safety_list_check (kActionsBlockedForSiteRisk) and a SendUserConfirmationDialogRequest whose denial yields kTriggeredNavigationBlocked.

Severity

medium — a browser-process authorization/consent-gate bypass with a demonstrated capability, not memory corruption, so higher rows do not apply. One proto field from a compromised guest permanently (per task, first-wins) disables the browser’s last-line constraints on a hostile agent — the Google-served Safety Lists block list and the mandated sensitive-origin consent dialog — enabling session-authenticated actuation on blocked and sensitive sites the user never approved (severity-guidelines S2: modify limited amounts of information). This matches Chromium triage precedent for the same class: CVE-2026-11187, CVE-2026-13896, CVE-2026-17805 (navigation-restriction bypass in Glic, all Medium). The eligibility preconditions and compromised-guest premise are consistent with medium; the finding strictly dominates the sibling added_writable_mainframe_origins report (cache seeding evaluated after the SafetyList, cannot suppress consent) accepted under the same premise.

Suggested Fix Direction

Make the guest-relayed config restrict-only: return kNoDecision instead of kAllowed on a rule match in EvaluateActorContainerConfig (so the SafetyList and consent predicates still run) while keeping kBlocked for non-matches — a change of this shape was verified to restore the blocked/consent behavior against the same payload. Longer term, do not accept agent_container_config over the unauthenticated guest transport, or order the SafetyList and consent predicates ahead of kActorContainerConfig.


POC: Glic Actor: guest-supplied SecurityMetadata.agent_container_config wildcard rule short-circuits the component-updater SafetyList (GLIC_ACTION_PAGE_BLOCK) and sensitive-origin user-confirmation dialog in OriginGatingChecker

Strategy: code-injection (attacker JavaScript injected at the Glic guest layer — the untrusted remote web client inside the chrome://glic <webview> — via --glic-guest-url; no Chromium source modification) Bug Type: authorization / user-consent bypass in the browser process (CWE-807: security decision made on renderer/guest-controlled input). No memory corruption; the oracle is behavioral (browser-side gating decisions + journal attributions), not ASan.

What the POC demonstrates

ActorKeyedService::PerformActions (chrome/browser/actor/actor_keyed_service.cc:778-797) installs the guest-supplied Actions.task_metadata.security.agent_container_config proto field, unauthenticated and first-wins, into the actor task’s OriginGatingChecker. That predicate (kActorContainerConfig, position 13 in ExecutionEngine’s predicate list) is evaluated before the component-updater “Actor Safety Lists” block list and before the sensitive-origin user-confirmation fallback, and returns a definitive allow/block. A compromised Glic guest that sends one forged wildcard rule (location: {wildcard: {}}, metadata: {accessible_resources: [RESOURCE_SESSION], capabilities: [CAPABILITY_ALL]}) therefore turns every subsequent navigation-commit gate and per-action gate into kAllowed — the GLIC_ACTION_PAGE_BLOCK/SafetyList layer is never consulted and the for_sensitive_origin consent dialog is never shown.

The POC runs the same actor scenario twice against the same browser configuration (same pre-seeded Actor Safety Lists block entry for http://localhost:8081; http://localhost:8082 is sensitive because kGlicActorLocalhostIsSensitive is default-enabled). The only difference is whether the guest’s first performActions message embeds the forged agent_container_config bytes:

Step control (no forged field) attack (forged wildcard)
NavigateAction → http://localhost:8081 (SafetyList block entry) kActionsBlockedForSiteRisk (44); journal: blocked / actor_safety_list_check kOk; page loads & is clicked; journal: allowed / ActorContainerConfig
ClickAction on that tab tab never left about:blank kOk; bank page reports bank-page-button-clicked
NavigateAction → http://localhost:8082 (sensitive origin) user-confirmation dialog raised (for_sensitive_origin); user denial ⇒ kTriggeredNavigationBlocked (27) kOk, no dialog; page loads & is clicked
Consent dialogs raised 1 0
Journal ActorContainerConfigSlot::Assign — status: assigned, active config: rules { Wildcard { RESOURCE_SESSION, CAPABILITY_ALL } }

The browser journal in the attack run also logs the received guest proto (base64 CiU6IwofaHR0cDovL2xvY2FsaG9zdDo4MDgxL2JhbmsuaHRtbBACEAEYASISChASDgoMCgIaABIGCgEBEgEB), which decodes exactly to actions {navigate {url:"http://localhost:8081/bank.html" tab_id:2}} task_id:1 skip_async_observation_collection:true task_metadata {security { agent_container_config {location_rules {location {wildcard {}} metadata {accessible_resources: RESOURCE_SESSION capabilities: CAPABILITY_ALL}}}}}.

Files

  • poc.html / poc.js — the compromised-guest stand-in. Plain web content; performs the production glic bootstrap handshake (glic-bootstrap postMessage from chrome://glic), registers as the web client, then drives createTask → createActorTab → performActions with hand-encoded optimization_guide.proto.Actions bytes. ?mode=control|attack selects whether the forged SecurityMetadata is included.
  • bank.html — target page served on :8081 (block-listed) and :8082 (sensitive); reports page load and button clicks to the log server.
  • server.py — serves guest (:8000) and targets (:8081/:8082); collects JSON reports.
  • run_poc.sh — one full round: seeds the profile + Actor Safety Lists component, launches Chrome, waits for the guest’s done report.
  • bandaid-fix.patch — verified minimal fix (see below).
  • evidence/ — captured results, screenshots, actor journal traces (.pftrace, perfetto; journal-extract-*.txt are strings extracts), component-load log lines, and the attack rerun against the patched build.

Prerequisites

  • Linux desktop Chromium build (tested against the ASAN build, out/asan/chrome; any build works — the bug is a logic flaw).
  • X display; ports 8000/8081/8082 free.

Reproduction

cd <artifacts-dir>
./run_poc.sh control /chromium_src/src/out/asan/chrome
./run_poc.sh attack  /chromium_src/src/out/asan/chrome
diff /tmp/glic-poc/logs/results-control.jsonl /tmp/glic-poc/logs/results-attack.jsonl

Each round takes ~90 s (the glic panel auto-opens ~30 s after startup). Outputs land in /tmp/glic-poc/logs/: results-<mode>.jsonl (guest + target page reports), actor-trace-<mode>.pftrace (browser actor journal; strings it and grep for attribution, ActorContainerConfigSlot, SendUserConfirmationDialogRequest), chrome-<mode>.log, screen-<mode>.png.

Environment scaffolding — what the flags simulate (and what they do not)

The control and attack rounds use the identical Chrome command line; only the guest payload differs. The flags stand in for preconditions that this offline, account-less sandbox cannot provide for real; none of them disables the two protections whose bypass is being demonstrated (the SafetyList predicate and the sensitive-origin confirmation dialog were demonstrably active in the control round):

  • --glic-guest-url=http://localhost:8000/poc.html?... — stands in for the attacker premise: a compromised Gemini web client (guest renderer / XSS on the guest origin). The POC page talks to the browser only through the production postMessage + Mojo relay path.
  • --enable-features=Glic — kGlic is FEATURE_ENABLED_BY_DEFAULT on Win/Mac/ChromeOS/Android; only the Linux dev platform defaults it off. GlicActor, GlicCrossOriginNavigationGating, GlicActorLocalhostIsSensitive etc. are default-enabled everywhere and are not toggled.
  • --glic-dev --glic-automation + GlicActor:glic_actor_policy_control_exemption/true
    • pre-seeded glic.user_enabled_actuation_on_web=true — stand in for the account-gated eligibility preconditions (signed-in account with can_use_model_execution_features / Gemini subscription tier, completed FRE, actuation setting on). Real Google accounts are not available in this environment; these are the in-tree dev/test knobs for exactly that precondition. They do not alter origin gating.
  • Pre-seeded ActorSafetyLists/9999.0.0.0/{manifest.json,listdata.json} in the profile directory — stands in for the Google-served component-updater “Actor Safety Lists” data (no network in the sandbox). Chrome’s component installer loads it through the normal on-disk registration path (Actor Safety Lists ready, version 9999.0.0.0 in both rounds’ logs) and block entries are enforced per the default kGlicEnforceComponentUpdaterBlockListEntries=true.
  • --no-proxy-server --variations-override-country=us --disable-features=GlicCountryFiltering,GlicWarming --no-first-run --no-default-browser-check --ignore-gpu-blocklist — sandbox/geo-rollout scaffolding only.
  • --actor-trace-path, --enable-logging, --vmodule — evidence capture.

--disable-actor-safety-checks is not used anywhere.

Expected Result

Control round (results-control.jsonl):

  • result-navigate-blocked-origin → kActionsBlockedForSiteRisk
  • user-confirmation-dialog-requested fires for http://localhost:8082, denial gives result-navigate-sensitive-origin → kTriggeredNavigationBlocked
  • no bank-page-loaded / bank-page-button-clicked reports
  • journal: blocked / actor_safety_list_check, SendUserConfirmationDialogRequest

Attack round (results-attack.jsonl):

  • journal: ActorContainerConfigSlot::Assign — status: assigned with the wildcard config
  • all four actions return kOk; both target pages report bank-page-loaded and bank-page-button-clicked
  • done reports dialogRequests: 0; zero SendUserConfirmationDialogRequest or actor_safety_list_check journal entries

Output (actual runs, ASAN build)

Control:

{"event": "task-created", "taskId": 1}
{"event": "actor-tab-created", "tabId": 2, "tabUrl": "about:blank"}
{"event": "result-navigate-blocked-origin", "actionResult": 44, "actionResultName": "kActionsBlockedForSiteRisk"}
{"event": "result-click-blocked-origin", "actionResult": 0, "actionResultName": "kOk"}   // tab still about:blank
{"event": "user-confirmation-dialog-requested", "navigationOrigin": "http://localhost:8082", "forBlocklistedOrigin": true}
{"event": "result-navigate-sensitive-origin", "actionResult": 27, "actionResultName": "kTriggeredNavigationBlocked"}
{"event": "result-click-sensitive-origin", "actionResult": 0, "actionResultName": "kOk"} // tab still about:blank
{"event": "done", "dialogRequests": 1}

Attack (same browser config; only the guest bytes differ):

{"event": "task-created", "taskId": 1}
{"event": "actor-tab-created", "tabId": 2, "tabUrl": "about:blank"}
{"who": "bank-page", "event": "bank-page-loaded", "origin": "http://localhost:8081"}
{"event": "result-navigate-blocked-origin", "actionResult": 0, "actionResultName": "kOk"}
{"who": "bank-page", "event": "bank-page-button-clicked", "origin": "http://localhost:8081"}
{"event": "result-click-blocked-origin", "actionResult": 0, "actionResultName": "kOk"}
{"who": "bank-page", "event": "bank-page-loaded", "origin": "http://localhost:8082"}
{"event": "result-navigate-sensitive-origin", "actionResult": 0, "actionResultName": "kOk"}
{"who": "bank-page", "event": "bank-page-button-clicked", "origin": "http://localhost:8082"}
{"event": "result-click-sensitive-origin", "actionResult": 0, "actionResultName": "kOk"}
{"event": "done", "dialogRequests": 0}

Attack-round journal (strings of evidence/actor-trace-attack.pftrace):

ActorContainerConfigSlot::Assign   status2 assigned
  active config2 { "rules": { "Wildcard": { "accessible_resources": [ "RESOURCE_SESSION" ],
                                            "capabilities": [ "CAPABILITY_ALL" ], ... } } }
OriginGatingDecision ... decision2 allowed  attribution2 ActorContainerConfig   (x4: both
  navigation responses and both page actions)

Control-round journal:

OriginGatingDecision ... decision2 blocked  attribution2 actor_safety_list_check
SendUserConfirmationDialogRequest
OriginGatingDecision ... decision2 blocked  attribution2 NoVerdict   (after user denial)

evidence/screen-attack.png shows the actor tab on the target page with the “Take over task” actor chip and the page’s “TRANSFER EXECUTED (button was clicked)” state — actuation on a consent-gated origin with no dialog shown.

Bandaid fix (verified)

bandaid-fix.patch changes OriginGatingChecker::EvaluateActorContainerConfig (components/origin_gating/core/origin_gating_checker.cc) so the guest-relayed config can only restrict, never grant: a matching rule now returns kNoDecision (evaluation continues into the SafetyList predicate and the consent fallback) while non-matching locations remain kBlocked, preserving the field’s documented server-side-restriction intent.

Verified: with the patch applied (autoninja -C out/asan chrome), the identical attack payload behaves exactly like the control round — kActionsBlockedForSiteRisk on the block-listed origin, consent dialog raised for the sensitive origin (see evidence/results-attack-with-bandaid-fix.jsonl). The source tree was reverted afterwards.


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