CVE-2026-113151
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/origin_gating/core/origin_gating_checker.cc |
modified |
Files Changed
components/origin_gating/core/types.cccomponents/origin_gating/core/origin_gating_checker_unittest.ccchrome/browser/actor/execution_engine_origin_gating_browsertest.cccomponents/origin_gating/core/origin_gating_checker.cccomponents/origin_gating/core/types.hchrome/browser/actor/execution_engine.cccomponents/origin_gating/core/origin_gating_checker.h
Patch
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)
Regression Test / PoC
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)
Original Bug Report
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 checkerchrome/browser/actor/actor_task_metadata.cc:42-45— verbatim copy ofsecurity().agent_container_config()from guest-suppliedActionsbyteschrome/browser/glic/actor/glic_actor_task_manager.cc:434-512— browser-side handler; validates onlytask_id, forwards metadata untouchedchrome/browser/actor/actor_proto_conversion.cc:1609-1635— purely syntactic conversion (wildcard at :1055-1057,CAPABILITY_ALL/RESOURCE_SESSIONat :1084-1105)chrome/browser/actor/execution_engine.cc:757-763—kActorContainerConfigpredicate ordered before the SafetyList predicate and confirmation cachecomponents/origin_gating/core/origin_gating_checker.cc:379-394— populated slot yields a definitive decision, halting predicate evaluationcomponents/origin_gating/core/actor_container_config_slot.cc:17-22— strictly first-wins slot; the guest-fed path is its only production assignercomponents/origin_gating/core/actor_container_config.cc:73-189— wildcard location matches any URL; empty sources match any source;CAPABILITY_ALL+RESOURCE_SESSIONallow 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 theGLIC_ACTION_PAGE_BLOCKoptimization-guide hint), where user denial would block the action; - the new-origin confirmation flow in the
OnNoVerdictfallback.
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
- Guest bytes reach the browser with only task_id validated:
chrome/browser/glic/actor/glic_actor_task_manager.cc:434-512— the guest’sperformActionsArrayBuffer 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 onlyValidateTaskIdMatchesCurrent, and forwardsactor::ActorTaskMetadata(actions)verbatim at:507. - Verbatim parse of the security policy field:
chrome/browser/actor/actor_task_metadata.cc:42-45— copiestask_metadata.security().agent_container_config()unmodified. - 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 checkActorContainerConfigSlot::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. - A wildcard rule grants everything:
components/origin_gating/core/actor_container_config.cc:73-189—Location::Matchesis unconditionally true forWildcard(:73-82); emptynavigation_sourcesmatches any source (:118-124);CanNavigate()is true withkAll+kSession(:126-129); bothIsActuationAllowedandIsNavigationAllowedfall through to the wildcard rule (:153-189). - Definitive decision short-circuits the safety layer:
components/origin_gating/core/origin_gating_checker.cc:379-394— with the slot populated, the predicate never returnskNoDecision, andProcessDecision(kAllowed)resolves the gate and stops evaluation (:81-104, :172-174). Inchrome/browser/actor/execution_engine.cc:757-763,kActorContainerConfigprecedesCreateSafetyListPredicate()andkCacheWithUserConfirmation, 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 viaActorNavigationThrottle(:850-889).
Reproduction
- On a Glic-eligible profile (signed-in eligible account, FRE completed;
kGlic/kGlicActor/kGlicCrossOriginNavigationGatingat their enabled defaults), run attacker JavaScript in the Glic guest webview (compromised guest renderer, or XSS on the Gemini client origin). - Perform the normal actor bootstrap (
createTask,createActorTab) to obtain a livetask_id. - Send a
performActionsmessage whose serializedActionsproto carries the validtask_id, any first action, andtask_metadata.security.agent_container_config.location_rules = [{location: {wildcard: {}}, metadata: {accessible_resources: [RESOURCE_SESSION], capabilities: [CAPABILITY_ALL]}}]. - Send further
performActionswithNavigateActionto an Actor-Safety-Lists block-listed origin and to a sensitive (GLIC_ACTION_PAGE_BLOCK-class) origin, followed byClickAction/TypeActionon those tabs. - Expected: the block-listed navigation fails with
kActionsBlockedForSiteRisk(SafetyList block), and the sensitive-origin action raises thefor_sensitive_originuser-confirmation dialog, whose denial blocks withkTriggeredNavigationBlocked.
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-bootstrappostMessage from chrome://glic), registers as the web client, then drivescreateTask→createActorTab→performActionswith hand-encodedoptimization_guide.proto.Actionsbytes.?mode=control|attackselects whether the forgedSecurityMetadatais 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’sdonereport.bandaid-fix.patch— verified minimal fix (see below).evidence/— captured results, screenshots, actor journal traces (.pftrace, perfetto;journal-extract-*.txtarestringsextracts), 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—kGlicis FEATURE_ENABLED_BY_DEFAULT on Win/Mac/ChromeOS/Android; only the Linux dev platform defaults it off.GlicActor,GlicCrossOriginNavigationGating,GlicActorLocalhostIsSensitiveetc. 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 withcan_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
- 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.0in both rounds’ logs) and block entries are enforced per the defaultkGlicEnforceComponentUpdaterBlockListEntries=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→kActionsBlockedForSiteRiskuser-confirmation-dialog-requestedfires forhttp://localhost:8082, denial givesresult-navigate-sensitive-origin→kTriggeredNavigationBlocked- no
bank-page-loaded/bank-page-button-clickedreports - journal:
blocked / actor_safety_list_check,SendUserConfirmationDialogRequest
Attack round (results-attack.jsonl):
- journal:
ActorContainerConfigSlot::Assign — status: assignedwith the wildcard config - all four actions return
kOk; both target pages reportbank-page-loadedandbank-page-button-clicked donereportsdialogRequests: 0; zeroSendUserConfirmationDialogRequestoractor_safety_list_checkjournal 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.