CVE-2026-78907
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TESTcomponents/enterprise/data_controls/core/browser/conditions/attributes_condition_unittest.cc |
modified |
Files Changed
components/enterprise/data_controls/core/browser/action_context.cccomponents/enterprise/data_controls/core/browser/conditions/attributes_condition_unittest.cc
Patch
From 8dc8ff8976a12c88b735f650fa536b192f8fd7eb Mon Sep 17 00:00:00 2001
From: Dominique Fauteux-Chapleau <domfc@chromium.org>
Date: Wed, 08 Jul 2026 14:03:53 -0700
Subject: [PATCH] [DataControls] Evaluate rules for tabs with no committed URL
ActionSource::empty() and ActionDestination::empty() only checked url,
os_clipboard and gemini_in_chrome, ignoring incognito and other_profile.
A tab on the initial empty document (e.g. a popup opened via a
javascript: URI) has an empty URL but can still have
incognito/other_profile set, so rules keyed on those attributes were
skipped for such tabs.
empty() now matches its documented contract and returns false when any
tab-related field is non-default. Existing AttributesConditionTest
expectations are updated to match.
Fixed: 503847023
Change-Id: I35a83737332d7c9ef08013ee49e544ea43161955
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8026769
Commit-Queue: Shanthanu Bhardwaj <xanth@google.com>
Reviewed-by: Shanthanu Bhardwaj <xanth@google.com>
Cr-Commit-Position: refs/heads/main@{#1659051}
---
diff --git a/components/enterprise/data_controls/core/browser/action_context.cc b/components/enterprise/data_controls/core/browser/action_context.cc
index 16a02593..ddb6dd3a 100644
--- a/components/enterprise/data_controls/core/browser/action_context.cc
+++ b/components/enterprise/data_controls/core/browser/action_context.cc
@@ -8,27 +8,29 @@
bool ActionSource::empty() const {
// `ActionSource` should represent either:
- // - A browser tab with the `url` field, and possible `incognito` and/or
- // `other_profile` set to true.
+ // - A browser tab with the `url`, `incognito` and/or `other_profile` fields.
+ // The `url` field can be empty if the tab has no committed navigation.
// - The OS clipboard with `os_clipboard` set to true.
// - The integrated Gemini browser agent (Glic) with `gemini_in_chrome` set
// to true.
- return url.is_empty() && !os_clipboard && !gemini_in_chrome;
+ return url.is_empty() && !incognito && !other_profile && !os_clipboard &&
+ !gemini_in_chrome;
}
bool ActionDestination::empty() const {
// `ActionDestination` should represent either:
- // - A browser tab with the `url` field, and possible `incognito` and/or
- // `other_profile` set to true.
+ // - A browser tab with the `url`, `incognito` and/or `other_profile` fields.
+ // The `url` field can be empty if the tab has no committed navigation.
// - The OS clipboard with `os_clipboard` set to true.
// - The integrated Gemini browser agent (Glic) with `gemini_in_chrome` set
// to true.
// - A separate application represented by `component` (CrOS-only).
#if BUILDFLAG(IS_CHROMEOS)
- return url.is_empty() && !os_clipboard && !gemini_in_chrome &&
- component == Component::kUnknownComponent;
+ return url.is_empty() && !incognito && !other_profile && !os_clipboard &&
+ !gemini_in_chrome && component == Component::kUnknownComponent;
#else
- return url.is_empty() && !os_clipboard && !gemini_in_chrome;
+ return url.is_empty() && !incognito && !other_profile && !os_clipboard &&
+ !gemini_in_chrome;
#endif // BUILDFLAG(IS_CHROMEOS)
}
diff --git a/components/enterprise/data_controls/core/browser/conditions/attributes_condition_unittest.cc b/components/enterprise/data_controls/core/browser/conditions/attributes_condition_unittest.cc
index 82c73c3..e829898 100644
--- a/components/enterprise/data_controls/core/browser/conditions/attributes_condition_unittest.cc
+++ b/components/enterprise/data_controls/core/browser/conditions/attributes_condition_unittest.cc
@@ -305,14 +305,15 @@
#endif // BUILDFLAG(IS_CHROMEOS)
TEST(AttributesConditionTest, IncognitoDestination) {
- // A context with only "incognito" and no URL shouldn't be evaluated.
+ // A destination tab can have `incognito` set even without a committed URL
+ // (e.g. the initial empty document), so a context with only `incognito` set
+ // should still be evaluated.
auto incognito_dst = DestinationAttributesCondition::Create(CreateDict(R"(
{
"incognito": true,
})"));
ASSERT_TRUE(incognito_dst);
- ASSERT_FALSE(
- incognito_dst->CanBeEvaluated({.destination = {.incognito = true}}));
+ ASSERT_TRUE(incognito_dst->IsTriggered({.destination = {.incognito = true}}));
ASSERT_FALSE(
incognito_dst->CanBeEvaluated({.destination = {.incognito = false}}));
ASSERT_FALSE(incognito_dst->CanBeEvaluated({.source = {.incognito = true}}));
@@ -323,9 +324,11 @@
"incognito": false,
})"));
ASSERT_TRUE(non_incognito_dst);
- ASSERT_FALSE(
+ ASSERT_TRUE(
non_incognito_dst->CanBeEvaluated({.destination = {.incognito = true}}));
ASSERT_FALSE(
+ non_incognito_dst->IsTriggered({.destination = {.incognito = true}}));
+ ASSERT_FALSE(
non_incognito_dst->CanBeEvaluated({.destination = {.incognito = false}}));
ASSERT_FALSE(
non_incognito_dst->CanBeEvaluated({.source = {.incognito = true}}));
@@ -334,7 +337,9 @@
}
TEST(AttributesConditionTest, IncognitoSource) {
- // A context with only "incognito" and no URL shouldn't be evaluated.
+ // A source tab can have `incognito` set even without a committed URL (e.g.
+ // the initial empty document), so a context with only `incognito` set should
+ // still be evaluated.
auto incognito_src = SourceAttributesCondition::Create(CreateDict(R"(
{
"incognito": true,
@@ -344,7 +349,7 @@
incognito_src->CanBeEvaluated({.destination = {.incognito = true}}));
ASSERT_FALSE(
incognito_src->CanBeEvaluated({.destination = {.incognito = false}}));
- ASSERT_FALSE(incognito_src->CanBeEvaluated({.source = {.incognito = true}}));
+ ASSERT_TRUE(incognito_src->IsTriggered({.source = {.incognito = true}}));
ASSERT_FALSE(incognito_src->CanBeEvaluated({.source = {.incognito = false}}));
auto non_incognito_src = SourceAttributesCondition::Create(CreateDict(R"(
@@ -356,8 +361,9 @@
non_incognito_src->CanBeEvaluated({.destination = {.incognito = true}}));
ASSERT_FALSE(
non_incognito_src->CanBeEvaluated({.destination = {.incognito = false}}));
- ASSERT_FALSE(
+ ASSERT_TRUE(
non_incognito_src->CanBeEvaluated({.source = {.incognito = true}}));
+ ASSERT_FALSE(non_incognito_src->IsTriggered({.source = {.incognito = true}}));
ASSERT_FALSE(
non_incognito_src->CanBeEvaluated({.source = {.incognito = false}}));
}
@@ -381,7 +387,7 @@
{.destination = {.url = GURL(kChromiumUrl), .incognito = false}}));
ASSERT_FALSE(url_and_incognito->IsTriggered(
{.destination = {.url = GURL(kChromiumUrl)}}));
- ASSERT_FALSE(
+ ASSERT_TRUE(
url_and_incognito->CanBeEvaluated({.destination = {.incognito = true}}));
ASSERT_FALSE(
url_and_incognito->CanBeEvaluated({.destination = {.incognito = false}}));
@@ -405,7 +411,7 @@
{.destination = {.url = GURL(kChromiumUrl), .incognito = false}}));
ASSERT_FALSE(url_and_not_incognito->IsTriggered(
{.destination = {.url = GURL(kChromiumUrl)}}));
- ASSERT_FALSE(url_and_not_incognito->CanBeEvaluated(
+ ASSERT_TRUE(url_and_not_incognito->CanBeEvaluated(
{.destination = {.incognito = true}}));
ASSERT_FALSE(url_and_not_incognito->CanBeEvaluated(
{.destination = {.incognito = false}}));
@@ -430,7 +436,7 @@
{.source = {.url = GURL(kChromiumUrl), .incognito = false}}));
ASSERT_FALSE(
url_and_incognito->IsTriggered({.source = {.url = GURL(kChromiumUrl)}}));
- ASSERT_FALSE(
+ ASSERT_TRUE(
url_and_incognito->CanBeEvaluated({.source = {.incognito = true}}));
ASSERT_FALSE(
url_and_incognito->CanBeEvaluated({.source = {.incognito = false}}));
@@ -453,7 +459,7 @@
{.source = {.url = GURL(kChromiumUrl), .incognito = false}}));
ASSERT_FALSE(url_and_not_incognito->IsTriggered(
{.source = {.url = GURL(kChromiumUrl)}}));
- ASSERT_FALSE(
+ ASSERT_TRUE(
url_and_not_incognito->CanBeEvaluated({.source = {.incognito = true}}));
ASSERT_FALSE(
url_and_not_incognito->CanBeEvaluated({.source = {.incognito = false}}));
@@ -472,7 +478,7 @@
ASSERT_TRUE(any_url->IsTriggered(
{.destination = {.url = GURL(kGoogleUrl), .incognito = false}}));
ASSERT_TRUE(any_url->IsTriggered({.destination = {.url = GURL(kGoogleUrl)}}));
- ASSERT_FALSE(any_url->CanBeEvaluated({.destination = {.incognito = true}}));
+ ASSERT_TRUE(any_url->CanBeEvaluated({.destination = {.incognito = true}}));
ASSERT_FALSE(any_url->CanBeEvaluated({.destination = {.incognito = false}}));
ASSERT_FALSE(any_url->CanBeEvaluated({.destination = {}}));
}
@@ -490,20 +496,22 @@
ASSERT_TRUE(any_url->IsTriggered(
{.source = {.url = GURL(kGoogleUrl), .incognito = false}}));
ASSERT_TRUE(any_url->IsTriggered({.source = {.url = GURL(kGoogleUrl)}}));
- ASSERT_FALSE(any_url->CanBeEvaluated({.source = {.incognito = true}}));
+ ASSERT_TRUE(any_url->CanBeEvaluated({.source = {.incognito = true}}));
ASSERT_FALSE(any_url->CanBeEvaluated({.source = {.incognito = false}}));
ASSERT_FALSE(any_url->CanBeEvaluated({.source = {}}));
}
TEST(AttributesConditionTest, OtherProfileDestination) {
- // A context with only "other_profile" and no URL shouldn't be evaluated.
+ // A destination tab can have `other_profile` set even without a committed
+ // URL (e.g. the initial empty document), so a context with only
+ // `other_profile` set should still be evaluated.
auto other_profile_dst = DestinationAttributesCondition::Create(CreateDict(R"(
{
"other_profile": true,
})"));
ASSERT_TRUE(other_profile_dst);
- ASSERT_FALSE(other_profile_dst->CanBeEvaluated(
- {.destination = {.other_profile = true}}));
Regression Test / PoC
diff --git a/components/enterprise/data_controls/core/browser/conditions/attributes_condition_unittest.cc b/components/enterprise/data_controls/core/browser/conditions/attributes_condition_unittest.cc
index 82c73c3..e829898 100644
--- a/components/enterprise/data_controls/core/browser/conditions/attributes_condition_unittest.cc
+++ b/components/enterprise/data_controls/core/browser/conditions/attributes_condition_unittest.cc
@@ -305,14 +305,15 @@
#endif // BUILDFLAG(IS_CHROMEOS)
TEST(AttributesConditionTest, IncognitoDestination) {
- // A context with only "incognito" and no URL shouldn't be evaluated.
+ // A destination tab can have `incognito` set even without a committed URL
+ // (e.g. the initial empty document), so a context with only `incognito` set
+ // should still be evaluated.
auto incognito_dst = DestinationAttributesCondition::Create(CreateDict(R"(
{
"incognito": true,
})"));
ASSERT_TRUE(incognito_dst);
- ASSERT_FALSE(
- incognito_dst->CanBeEvaluated({.destination = {.incognito = true}}));
+ ASSERT_TRUE(incognito_dst->IsTriggered({.destination = {.incognito = true}}));
ASSERT_FALSE(
incognito_dst->CanBeEvaluated({.destination = {.incognito = false}}));
ASSERT_FALSE(incognito_dst->CanBeEvaluated({.source = {.incognito = true}}));
@@ -323,9 +324,11 @@
"incognito": false,
})"));
ASSERT_TRUE(non_incognito_dst);
- ASSERT_FALSE(
+ ASSERT_TRUE(
non_incognito_dst->CanBeEvaluated({.destination = {.incognito = true}}));
ASSERT_FALSE(
+ non_incognito_dst->IsTriggered({.destination = {.incognito = true}}));
+ ASSERT_FALSE(
non_incognito_dst->CanBeEvaluated({.destination = {.incognito = false}}));
ASSERT_FALSE(
non_incognito_dst->CanBeEvaluated({.source = {.incognito = true}}));
@@ -334,7 +337,9 @@
}
TEST(AttributesConditionTest, IncognitoSource) {
- // A context with only "incognito" and no URL shouldn't be evaluated.
+ // A source tab can have `incognito` set even without a committed URL (e.g.
+ // the initial empty document), so a context with only `incognito` set should
+ // still be evaluated.
auto incognito_src = SourceAttributesCondition::Create(CreateDict(R"(
{
"incognito": true,
@@ -344,7 +349,7 @@
incognito_src->CanBeEvaluated({.destination = {.incognito = true}}));
ASSERT_FALSE(
incognito_src->CanBeEvaluated({.destination = {.incognito = false}}));
- ASSERT_FALSE(incognito_src->CanBeEvaluated({.source = {.incognito = true}}));
+ ASSERT_TRUE(incognito_src->IsTriggered({.source = {.incognito = true}}));
ASSERT_FALSE(incognito_src->CanBeEvaluated({.source = {.incognito = false}}));
auto non_incognito_src = SourceAttributesCondition::Create(CreateDict(R"(
@@ -356,8 +361,9 @@
non_incognito_src->CanBeEvaluated({.destination = {.incognito = true}}));
ASSERT_FALSE(
non_incognito_src->CanBeEvaluated({.destination = {.incognito = false}}));
- ASSERT_FALSE(
+ ASSERT_TRUE(
non_incognito_src->CanBeEvaluated({.source = {.incognito = true}}));
+ ASSERT_FALSE(non_incognito_src->IsTriggered({.source = {.incognito = true}}));
ASSERT_FALSE(
non_incognito_src->CanBeEvaluated({.source = {.incognito = false}}));
}
@@ -381,7 +387,7 @@
{.destination = {.url = GURL(kChromiumUrl), .incognito = false}}));
ASSERT_FALSE(url_and_incognito->IsTriggered(
{.destination = {.url = GURL(kChromiumUrl)}}));
- ASSERT_FALSE(
+ ASSERT_TRUE(
url_and_incognito->CanBeEvaluated({.destination = {.incognito = true}}));
ASSERT_FALSE(
url_and_incognito->CanBeEvaluated({.destination = {.incognito = false}}));
@@ -405,7 +411,7 @@
{.destination = {.url = GURL(kChromiumUrl), .incognito = false}}));
ASSERT_FALSE(url_and_not_incognito->IsTriggered(
{.destination = {.url = GURL(kChromiumUrl)}}));
- ASSERT_FALSE(url_and_not_incognito->CanBeEvaluated(
+ ASSERT_TRUE(url_and_not_incognito->CanBeEvaluated(
{.destination = {.incognito = true}}));
ASSERT_FALSE(url_and_not_incognito->CanBeEvaluated(
{.destination = {.incognito = false}}));
@@ -430,7 +436,7 @@
{.source = {.url = GURL(kChromiumUrl), .incognito = false}}));
ASSERT_FALSE(
url_and_incognito->IsTriggered({.source = {.url = GURL(kChromiumUrl)}}));
- ASSERT_FALSE(
+ ASSERT_TRUE(
url_and_incognito->CanBeEvaluated({.source = {.incognito = true}}));
ASSERT_FALSE(
url_and_incognito->CanBeEvaluated({.source = {.incognito = false}}));
@@ -453,7 +459,7 @@
{.source = {.url = GURL(kChromiumUrl), .incognito = false}}));
ASSERT_FALSE(url_and_not_incognito->IsTriggered(
{.source = {.url = GURL(kChromiumUrl)}}));
- ASSERT_FALSE(
+ ASSERT_TRUE(
url_and_not_incognito->CanBeEvaluated({.source = {.incognito = true}}));
ASSERT_FALSE(
url_and_not_incognito->CanBeEvaluated({.source = {.incognito = false}}));
@@ -472,7 +478,7 @@
ASSERT_TRUE(any_url->IsTriggered(
{.destination = {.url = GURL(kGoogleUrl), .incognito = false}}));
ASSERT_TRUE(any_url->IsTriggered({.destination = {.url = GURL(kGoogleUrl)}}));
- ASSERT_FALSE(any_url->CanBeEvaluated({.destination = {.incognito = true}}));
+ ASSERT_TRUE(any_url->CanBeEvaluated({.destination = {.incognito = true}}));
ASSERT_FALSE(any_url->CanBeEvaluated({.destination = {.incognito = false}}));
ASSERT_FALSE(any_url->CanBeEvaluated({.destination = {}}));
}
@@ -490,20 +496,22 @@
ASSERT_TRUE(any_url->IsTriggered(
{.source = {.url = GURL(kGoogleUrl), .incognito = false}}));
ASSERT_TRUE(any_url->IsTriggered({.source = {.url = GURL(kGoogleUrl)}}));
- ASSERT_FALSE(any_url->CanBeEvaluated({.source = {.incognito = true}}));
+ ASSERT_TRUE(any_url->CanBeEvaluated({.source = {.incognito = true}}));
ASSERT_FALSE(any_url->CanBeEvaluated({.source = {.incognito = false}}));
ASSERT_FALSE(any_url->CanBeEvaluated({.source = {}}));
}
TEST(AttributesConditionTest, OtherProfileDestination) {
- // A context with only "other_profile" and no URL shouldn't be evaluated.
+ // A destination tab can have `other_profile` set even without a committed
+ // URL (e.g. the initial empty document), so a context with only
+ // `other_profile` set should still be evaluated.
auto other_profile_dst = DestinationAttributesCondition::Create(CreateDict(R"(
{
"other_profile": true,
})"));
ASSERT_TRUE(other_profile_dst);
- ASSERT_FALSE(other_profile_dst->CanBeEvaluated(
- {.destination = {.other_profile = true}}));
+ ASSERT_TRUE(
+ other_profile_dst->IsTriggered({.destination = {.other_profile = true}}));
ASSERT_FALSE(other_profile_dst->CanBeEvaluated(
{.destination = {.other_profile = false}}));
ASSERT_FALSE(
@@ -517,7 +525,9 @@
"other_profile": false,
})"));
ASSERT_TRUE(non_other_profile_dst);
- ASSERT_FALSE(non_other_profile_dst->CanBeEvaluated(
+ ASSERT_TRUE(non_other_profile_dst->CanBeEvaluated(
+ {.destination = {.other_profile = true}}));
+ ASSERT_FALSE(non_other_profile_dst->IsTriggered(
{.destination = {.other_profile = true}}));
ASSERT_FALSE(non_other_profile_dst->CanBeEvaluated(
{.destination = {.other_profile = false}}));
@@ -528,7 +538,9 @@
}
TEST(AttributesConditionTest, OtherProfileSource) {
- // A context with only "other_profile" and no URL shouldn't be evaluated.
+ // A source tab can have `other_profile` set even without a committed URL
+ // (e.g. the initial empty document), so a context with only `other_profile`
+ // set should still be evaluated.
auto other_profile_src = SourceAttributesCondition::Create(CreateDict(R"(
{
"other_profile": true,
@@ -538,8 +550,8 @@
{.destination = {.other_profile = true}}));
ASSERT_FALSE(other_profile_src->CanBeEvaluated(
{.destination = {.other_profile = false}}));
- ASSERT_FALSE(
- other_profile_src->CanBeEvaluated({.source = {.other_profile = true}}));
+ ASSERT_TRUE(
+ other_profile_src->IsTriggered({.source = {.other_profile = true}}));
ASSERT_FALSE(
other_profile_src->CanBeEvaluated({.source = {.other_profile = false}}));
@@ -552,8 +564,10 @@
{.destination = {.other_profile = true}}));
ASSERT_FALSE(non_other_profile_src->CanBeEvaluated(
{.destination = {.other_profile = false}}));
- ASSERT_FALSE(non_other_profile_src->CanBeEvaluated(
+ ASSERT_TRUE(non_other_profile_src->CanBeEvaluated(
{.source = {.other_profile = true}}));
+ ASSERT_FALSE(
+ non_other_profile_src->IsTriggered({.source = {.other_profile = true}}));
ASSERT_FALSE(non_other_profile_src->CanBeEvaluated(
{.source = {.other_profile = false}}));
}
@@ -578,7 +592,7 @@
{.destination = {.url = GURL(kChromiumUrl), .other_profile = false}}));
ASSERT_FALSE(url_and_other_profile->IsTriggered(
{.destination = {.url = GURL(kChromiumUrl)}}));
- ASSERT_FALSE(url_and_other_profile->CanBeEvaluated(
+ ASSERT_TRUE(url_and_other_profile->CanBeEvaluated(
{.destination = {.other_profile = true}}));
ASSERT_FALSE(url_and_other_profile->CanBeEvaluated(
{.destination = {.other_profile = false}}));
@@ -602,7 +616,7 @@
{.destination = {.url = GURL(kChromiumUrl), .other_profile = false}}));
ASSERT_FALSE(url_and_not_other_profile->IsTriggered(
{.destination = {.url = GURL(kChromiumUrl)}}));
- ASSERT_FALSE(url_and_not_other_profile->CanBeEvaluated(
+ ASSERT_TRUE(url_and_not_other_profile->CanBeEvaluated(
{.destination = {.other_profile = true}}));
ASSERT_FALSE(url_and_not_other_profile->CanBeEvaluated(
{.destination = {.other_profile = false}}));
@@ -627,7 +641,7 @@
{.source = {.url = GURL(kChromiumUrl), .other_profile = false}}));
ASSERT_FALSE(url_and_other_profile->IsTriggered(
{.source = {.url = GURL(kChromiumUrl)}}));
- ASSERT_FALSE(url_and_other_profile->CanBeEvaluated(
+ ASSERT_TRUE(url_and_other_profile->CanBeEvaluated(
{.source = {.other_profile = true}}));
ASSERT_FALSE(url_and_other_profile->CanBeEvaluated(
{.source = {.other_profile = false}}));
@@ -651,7 +665,7 @@
{.source = {.url = GURL(kChromiumUrl), .other_profile = false}}));
ASSERT_FALSE(url_and_not_other_profile->IsTriggered(
{.source = {.url = GURL(kChromiumUrl)}}));
- ASSERT_FALSE(url_and_not_other_profile->CanBeEvaluated(
+ ASSERT_TRUE(url_and_not_other_profile->CanBeEvaluated(
{.source = {.other_profile = true}}));
ASSERT_FALSE(url_and_not_other_profile->CanBeEvaluated(
{.source = {.other_profile = false}}));
@@ -670,7 +684,7 @@
ASSERT_TRUE(any_url->IsTriggered(
{.destination = {.url = GURL(kGoogleUrl), .other_profile = false}}));
ASSERT_TRUE(any_url->IsTriggered({.destination = {.url = GURL(kGoogleUrl)}}));
- ASSERT_FALSE(
+ ASSERT_TRUE(
any_url->CanBeEvaluated({.destination = {.other_profile = true}}));
ASSERT_FALSE(
any_url->CanBeEvaluated({.destination = {.other_profile = false}}));
@@ -690,7 +704,7 @@
ASSERT_TRUE(any_url->IsTriggered(
{.source = {.url = GURL(kGoogleUrl), .other_profile = false}}));
ASSERT_TRUE(any_url->IsTriggered({.source = {.url = GURL(kGoogleUrl)}}));
- ASSERT_FALSE(any_url->CanBeEvaluated({.source = {.other_profile = true}}));
+ ASSERT_TRUE(any_url->CanBeEvaluated({.source = {.other_profile = true}}));
ASSERT_FALSE(any_url->CanBeEvaluated({.source = {.other_profile = false}}));
ASSERT_FALSE(any_url->CanBeEvaluated({.source = {}}));
}
@@ -723,10 +737,10 @@
{.source = {.url = GURL(kChromiumUrl), .other_profile = true}}));
ASSERT_FALSE(condition->IsTriggered({.source = {.url = GURL(kGoogleUrl)}}));
ASSERT_FALSE(condition->IsTriggered({.source = {.url = GURL(kChromiumUrl)}}));
- ASSERT_FALSE(condition->CanBeEvaluated({.source = {.incognito = true}}));
+ ASSERT_TRUE(condition->CanBeEvaluated({.source = {.incognito = true}}));
ASSERT_FALSE(condition->CanBeEvaluated({.source = {.incognito = false}}));
- ASSERT_FALSE(condition->CanBeEvaluated({.source = {.other_profile = true}}));
+ ASSERT_TRUE(condition->CanBeEvaluated({.source = {.other_profile = true}}));
ASSERT_FALSE(condition->CanBeEvaluated({.source = {.other_profile = false}}));
}
@@ -761,11 +775,11 @@
condition->IsTriggered({.destination = {.url = GURL(kGoogleUrl)}}));
ASSERT_FALSE(
condition->IsTriggered({.destination = {.url = GURL(kChromiumUrl)}}));
- ASSERT_FALSE(condition->CanBeEvaluated({.destination = {.incognito = true}}));
+ ASSERT_TRUE(condition->CanBeEvaluated({.destination = {.incognito = true}}));
ASSERT_FALSE(
condition->CanBeEvaluated({.destination = {.incognito = false}}));
- ASSERT_FALSE(
+ ASSERT_TRUE(
condition->CanBeEvaluated({.destination = {.other_profile = true}}));
ASSERT_FALSE(
condition->CanBeEvaluated({.destination = {.other_profile = false}}));
Original Bug Report
Universal Enterprise Policy Bypass via Empty URL Contexts in Data Controls
Flapjack, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.
Overview: A logic flaw in Enterprise Data Controls allows users to bypass cross-profile and incognito paste restrictions. When a paste occurs in a tab with an empty committed URL (e.g., opened via a javascript: URI), the policy engine incorrectly determines the context is empty and skips evaluating the blocking rules. This enables exfiltration of sensitive data to unauthorized profiles.
Affected files:
components/enterprise/data_controls/core/browser/action_context.cccomponents/enterprise/data_controls/core/browser/conditions/attributes_condition.cc
Estimated timestamp from git blame: 2024-03-21
Description
A logic flaw in the Enterprise Data Controls engine allows for a potential universal bypass of policy restrictions when an action (like a clipboard paste) occurs in a context with an empty URL. The engine incorrectly determines that such a context is “empty” and consequently skips the evaluation of rules that should apply based on other attributes, such as cross-profile or Incognito status.
In components/enterprise/data_controls/core/browser/action_context.cc, the ActionSource::empty() and ActionDestination::empty() methods check only the URL and the OS clipboard flag, while completely ignoring the incognito and other_profile fields:
bool ActionSource::empty() const {
return url.is_empty() && !os_clipboard;
}
This implementation assumes that a valid browser tab context must have a non-empty URL. However, a context can be valid and restricted (e.g., belonging to an unmanaged profile) even if the URL is an empty GURL(). When empty() returns true, DestinationAttributesCondition::CanBeEvaluated() returns false, causing the policy engine to skip the condition entirely and allow the action.
An attacker or malicious page can intentionally create a tab with an empty last_committed_url_ by opening a new window and executing a javascript: URI. Because javascript: URIs are executed without committing a standard navigation, the RenderFrameHost’s last_committed_url_ remains uninitialized (GURL()).
Potential Exploitation Scenario
This flaw can be exploited to bypass cross-profile Data Loss Prevention (DLP) rules:
- Policy: An administrator sets a rule to block clipboard actions when the destination is another profile (e.g.,
{"destinations": {"other_profile": true}, "restrictions": [{"class": "CLIPBOARD", "level": "BLOCK"}]}). - Preparation: In an unmanaged, secondary browser profile, a user or a webpage opens a new window using JavaScript:
window.open('javascript:document.write("<body contenteditable></body>")'). This creates a newRenderFrameHostwhoselast_committed_url_is an emptyGURL(). - Copy: The user copies sensitive data from a tab within the managed profile.
- Paste: The user switches to the unmanaged profile and pastes the data into the blank,
contenteditabletab. - Bypass:
- The browser calls
CreateDataEndpoint()to get the destination URL, which returnsstd::nulloptbecauselast_committed_url_is empty. ExtractPasteActionContextsuccessfully detects the destination is a different profile and setsaction.other_profile = true, but leavesaction.urlempty.ActionDestination::empty()returnstruebecause the URL is empty and it ignoresother_profile.- The
DestinationAttributesConditionfails to evaluate, the block rule is skipped, and the sensitive data is successfully exfiltrated to the unauthorized profile.
- The browser calls
(Note: These are suggested steps based on static analysis. Our tooling agent does not currently have the capability to run a working Proof of Concept).
Recommended Fix
The empty() methods for ActionSource and ActionDestination in components/enterprise/data_controls/core/browser/action_context.cc must be updated to account for the incognito and other_profile flags. A context should only be considered empty if all its identifying attributes indicate no context is present.
bool ActionSource::empty() const {
return url.is_empty() && !os_clipboard && !incognito && !other_profile;
}
(Apply the equivalent fix to ActionDestination::empty().)
Evaluated with Chrome root at commit: c1eba8ce379f5d218a2948fafbb5dd72cfa30529
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.
Raised in root component due to access or custom field issues on 1974931