CVE-2026-13865
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forchrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle.cc |
modified | |
ifchrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle.cc |
modified | |
WillRedirectRequestchrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle.cc |
modified | |
WillProcessResponsechrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle.cc |
modified | |
GetOidcEnrollmentUrlMatcherForTestingchrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle.cc |
modified | |
AttemptToTriggerUrlInterceptionchrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle.cc |
modified |
Files Changed
chrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle.cc
Patch
From 3c576832ba7984e87e93cad7edf7597ed9dce0c3 Mon Sep 17 00:00:00 2001 From: Zonghan Xu <[email protected]> Date: Tue, 26 May 2026 07:13:24 -0700 Subject: [PATCH] [OIDC Enrollment] Remove deprecated OIDC enrollment method Remove the old method to register and create OIDC profiles using "https://chromeenterprise.google/enroll" URL. The server-side support for this method has already been removed a while ago. Bug: 512162479, 497090912, 381117479 Change-Id: Ic4ba9205f0baa7221df4cd245a558556b55082fa Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7859754 Reviewed-by: Sebastien Lalancette <[email protected]> Reviewed-by: David Roger <[email protected]> Reviewed-by: Hamda Mare <[email protected]> Commit-Queue: Zonghan Xu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1636150} --- diff --git a/chrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle.cc b/chrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle.cc index ef58d81..fbfd3835 100644 --- a/chrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle.cc +++ b/chrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle.cc @@ -38,15 +38,6 @@ namespace { -constexpr char kEnrollmentFallbackUrl[] = - "https://chromeenterprise.google/enroll"; - -// We consider this common host for Microsoft authentication to be valid -// redirection source. -constexpr char kEntraLoginHost[] = "https://login.microsoftonline.com"; -// Valid redirection from MSFT Cloud App Security portal. -constexpr char kEntraMcasHost[] = "https://mcas.ms"; - // Chrome Enterprise page that handles OIDC authentication redirection, this // page should receive the proper payload in its auth header to start OIDC // profile creation/registration. @@ -55,50 +46,10 @@ constexpr char kRegistrationHeaderField[] = "X-Profile-Registration-Payload"; -constexpr char kQuerySeparator[] = "&"; -constexpr char kKeyValueSeparator[] = "="; -constexpr char kAuthTokenHeader[] = "access_token"; -constexpr char kIdTokenHeader[] = "id_token"; -constexpr char kOidcStateHeader[] = "state"; - constexpr char kPayloadIssuerFieldName[] = "issuer"; constexpr char kPayloadSubjectFieldName[] = "subject"; constexpr char kPayloadCodeFieldName[] = "encrypted_user_information"; -base::flat_map<std::string, std::string> SplitUrl(const std::string& url) { - std::vector<std::string> fragments = base::SplitString( - url, kQuerySeparator, base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); - base::flat_map<std::string, std::string> url_map; - for (auto& fragment : fragments) { - size_t start = fragment.find(kKeyValueSeparator); - if (start == std::string::npos) { - continue; - } - std::string key = fragment.substr(0, start); - std::string val = fragment.substr(start + 1, fragment.size()); - url_map.emplace(key, val); - } - - return url_map; -} - -std::unique_ptr<URLMatcher> CreateEnrollmentRedirectUrlMatcher() { - auto matcher = std::make_unique<URLMatcher>(); - url_matcher::util::AddAllowFiltersWithLimit( - matcher.get(), std::vector<std::string>({kEnrollmentFallbackUrl})); - return matcher; -} - -const url_matcher::URLMatcher* GetEnrollmentRedirectUrlMatcher() { - static base::NoDestructor<std::unique_ptr<URLMatcher>> matcher( - CreateEnrollmentRedirectUrlMatcher()); - return matcher->get(); -} - -bool IsEnrollmentUrl(GURL& url) { - return !GetEnrollmentRedirectUrlMatcher()->MatchURL(url).empty(); -} - std::unique_ptr<URLMatcher> CreateEnrollmentHeaderUrlMatcher() { auto matcher = std::make_unique<URLMatcher>(); @@ -137,31 +88,6 @@ return !GetEnrollmentHeaderUrlMatcher()->MatchURL(url).empty(); } -std::unique_ptr<URLMatcher> CreateOidcEnrollmentUrlMatcher() { - auto matcher = std::make_unique<URLMatcher>(); - - std::vector<std::string> allowed_hosts({kEntraLoginHost, kEntraMcasHost}); - if (base::FeatureList::IsEnabled( - profile_management::features::kOidcEnrollmentAuthSource)) { - const std::vector<std::string>& hosts = base::SplitString( - profile_management::features::kOidcAuthAdditionalHosts.Get(), ",", - base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); - - for (const std::string& host : hosts) { - allowed_hosts.push_back(host); - } - } - - url_matcher::util::AddAllowFiltersWithLimit(matcher.get(), allowed_hosts); - return matcher; -} - -const url_matcher::URLMatcher* GetOidcEnrollmentUrlMatcher() { - static base::NoDestructor<std::unique_ptr<URLMatcher>> matcher( - CreateOidcEnrollmentUrlMatcher()); - return matcher->get(); -} - bool IsProfileValidForOidcEnrollment(Profile* profile) { // OIDC enrollment cannot be initiated from an incognito or guest profile. if (!profile || profile->IsOffTheRecord() || profile->IsGuestSession()) { @@ -174,15 +100,6 @@ return true; } -void RecordUntrustedRedirectChain( - content::NavigationHandle& navigation_handle) { - ukm::SourceId source_id = ukm::ConvertToSourceId( - navigation_handle.GetNavigationId(), ukm::SourceIdType::NAVIGATION_ID); - ukm::builders::Enterprise_Profile_Enrollment(source_id) - .SetIsUntrustedOidcRedirect(true) - .Record(ukm::UkmRecorder::Get()); -} - } // namespace namespace profile_management { @@ -207,206 +124,20 @@ ~OidcAuthResponseCaptureNavigationThrottle() = default; ThrottleCheckResult -OidcAuthResponseCaptureNavigationThrottle::WillRedirectRequest() { - return AttemptToTriggerUrlInterception(); -} - -ThrottleCheckResult OidcAuthResponseCaptureNavigationThrottle::WillProcessResponse() { - ThrottleCheckResult header_enrollment_check_result = PROCEED; if (base::FeatureList::IsEnabled( profile_management::features::kOidcAuthHeaderInterception)) { - header_enrollment_check_result = AttemptToTriggerHeaderInterception(); + return AttemptToTriggerHeaderInterception(); } - // Skip the URL interception attempt if a header interception was successful, - // or if response capturing is not enabled. - return (base::FeatureList::IsEnabled( - profile_management::features::kOidcAuthResponseInterception) && - header_enrollment_check_result.action() == PROCEED) - ? AttemptToTriggerUrlInterception() - : header_enrollment_check_result; + return PROCEED; } const char* OidcAuthResponseCaptureNavigationThrottle::GetNameForLogging() { return "OidcAuthResponseCaptureNavigationThrottle"; } -// static -std::unique_ptr<URLMatcher> OidcAuthResponseCaptureNavigationThrottle:: - GetOidcEnrollmentUrlMatcherForTesting() { - return CreateOidcEnrollmentUrlMatcher(); -} -ThrottleCheckResult -OidcAuthResponseCaptureNavigationThrottle::AttemptToTriggerUrlInterception() { - if (interception_triggered_) { - return PROCEED; - } - - if (navigation_handle()->GetRedirectChain().empty()) { - return PROCEED; - } - - auto url = navigation_handle()->GetURL(); - // Only try kicking off OIDC enrollment process if a valid enroll URL is seen. - if (!IsEnrollmentUrl(url)) { - return PROCEED; - } - - VLOG_POLICY(1, OIDC_ENROLLMENT) - << "Valid enrollment URL from OIDC redirection is found: " << url; - - if (!base::FeatureList::IsEnabled( - profile_management::features:: - kEnableGenericOidcAuthProfileManagement)) { - bool accept_redirect = false; - - for (const auto& chain_url : navigation_handle()->GetRedirectChain()) { - if (!GetOidcEnrollmentUrlMatcher()->MatchURL(chain_url).empty()) { - accept_redirect = true;
Regression Test / PoC
diff --git a/chrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle_browsertest.cc b/chrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle_browsertest.cc
index 73ab6bf4..77ab5c6 100644
--- a/chrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle_browsertest.cc
+++ b/chrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle_browsertest.cc
@@ -6,12 +6,9 @@
#include <memory>
-#include "base/base64.h"
#include "base/base64url.h"
-#include "base/json/json_writer.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
-#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/browser_process.h"
@@ -34,8 +31,6 @@
#include "net/dns/mock_host_resolver.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_util.h"
-#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
-#include "services/metrics/public/cpp/ukm_builders.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -46,86 +41,14 @@
namespace {
-constexpr char kTokenTemplate[] = R"(%s.%s.%s)";
-constexpr char kOidcAuthTokenFieldTemplate[] = R"(access_token=%s&)";
-constexpr char kOidcIdTokenFieldTemplate[] = R"(&id_token=%s&)";
-constexpr char kOidcStateFieldTemplate[] = R"(&state=%s&)";
-constexpr char kOidcAuthResponseTemplate[] =
- "https://chromeenterprise.google/enroll/"
- "#%stoken_type=Bearer&expires_in=5000&scope=email+openid+profile%s%"
- "ssession_"
- "state=abc-123";
-constexpr char kDummyHeader[] = "encoded_header";
-constexpr char kDummySignature[] = "signature";
-
-// constexpr char kRegistrationHeaderField[] = "X-Profile-Registration-Payload";
-
-constexpr char kOidcEntraReprocessUrl[] =
- "https://login.microsoftonline.com/common/"
- "reprocess?some_encoded_value&session_id=123";
-constexpr char kOidcNonEntraReprocessUrl[] =
- "https://test.com/common/reprocess?some_encoded_value&session_id=123";
-
-constexpr char kOidcEntraKmsiUrl[] = "https://login.microsoftonline.com/kmsi";
-constexpr char kOidcState[] = "1234";
-
constexpr char kHeaderInterceptionTestUrl[] =
"https://chromeenterprise.google/profile-enrollment/register-handler";
-constexpr char kUserPrincipleNameClaimName[] = "upn";
-constexpr char kSubjectClaimName[] = "sub";
-constexpr char kIssuerClaimName[] = "iss";
-
-constexpr char kExampleUserPrincipleName[] = "[email protected]";
-constexpr char kExampleAuthSubject[] = "example_auth_subject";
constexpr char kExampleIdSubject[] = "example_id_subject";
constexpr char kExampleIdIssuer[] = "example_id_issuer";
constexpr char kExampleEncodedInfo[] = "EncodedMessageInBase64";
constexpr char kOidcEnrollmentHistogramName[] = "Enterprise.OidcEnrollment";
-constexpr char kProfileEnrollmentUkm[] = "Enterprise.Profile.Enrollment";
-
-std::string BuildTokenFromDict(const base::DictValue& dict) {
- return base::StringPrintf(
- kTokenTemplate, kDummyHeader,
- base::Base64Encode(base::WriteJson(dict).value()).c_str(),
- kDummySignature);
-}
-
-std::string BuildOidcResponseUrl(const std::string& oidc_auth_token,
- const std::string& oidc_id_token,
- const std::string& oidc_state) {
- std::string auth_token_field =
- oidc_auth_token.empty() ? std::string()
- : base::StringPrintf(kOidcAuthTokenFieldTemplate,
- oidc_auth_token.c_str());
- std::string id_token_field =
- oidc_id_token.empty() ? std::string()
- : base::StringPrintf(kOidcIdTokenFieldTemplate,
- oidc_id_token.c_str());
- std::string state_field =
- oidc_state.empty()
- ? std::string()
- : base::StringPrintf(kOidcStateFieldTemplate, oidc_state.c_str());
-
- return base::StringPrintf(kOidcAuthResponseTemplate, auth_token_field.c_str(),
- id_token_field.c_str(), state_field.c_str());
-}
-
-// Convenient helper function that builds valid OIDC response URL using valid
-// tokens
-std::string BuildStandardResponseUrl(const std::string& oidc_state) {
- std::string auth_token = BuildTokenFromDict(
- base::DictValue()
- .Set(kUserPrincipleNameClaimName, kExampleUserPrincipleName)
- .Set(kSubjectClaimName, kExampleAuthSubject));
- std::string id_token = BuildTokenFromDict(
- base::DictValue()
- .Set(kUserPrincipleNameClaimName, kExampleUserPrincipleName)
- .Set(kSubjectClaimName, kExampleIdSubject)
- .Set(kIssuerClaimName, kExampleIdIssuer));
- return BuildOidcResponseUrl(auth_token, id_token, oidc_state);
-}
scoped_refptr<net::HttpResponseHeaders> BuildExampleResponseHeader(
std::string issuer = kExampleIdIssuer,
@@ -164,13 +87,6 @@
namespace profile_management {
-bool IsSourceUrlValid(std::string url_string) {
- return !OidcAuthResponseCaptureNavigationThrottle::
- GetOidcEnrollmentUrlMatcherForTesting()
- ->MatchURL(GURL(url_string))
- .empty();
-}
-
void ExpandFeatureList(std::vector<FeatureRefAndParams>& enabled_features,
std::vector<FeatureRef>& disabled_features,
const base::flat_map<FeatureRef, bool>& feature_states) {
@@ -184,44 +100,30 @@
}
class OidcAuthResponseCaptureNavigationThrottleTest
- : public InProcessBrowserTest,
- public testing::WithParamInterface<std::tuple<bool, bool, bool>> {
+ : public InProcessBrowserTest {
public:
+ OidcAuthResponseCaptureNavigationThrottleTest()
+ : OidcAuthResponseCaptureNavigationThrottleTest(true) {}
+
+ ~OidcAuthResponseCaptureNavigationThrottleTest() override = default;
+
+ protected:
explicit OidcAuthResponseCaptureNavigationThrottleTest(
- const std::string& additional_hosts) {
+ bool enable_oidc_interception) {
std::vector<FeatureRefAndParams> enabled_features;
std::vector<FeatureRef> disabled_features;
ExpandFeatureList(
enabled_features, disabled_features,
- {{features::kOidcAuthProfileManagement, enable_oidc_interception()},
- {features::kEnableGenericOidcAuthProfileManagement,
- enable_generic_oidc()},
- {features::kOidcAuthResponseInterception, enable_process_response()}});
-
- if (!additional_hosts.empty()) {
- enabled_features.push_back(
- {features::kOidcEnrollmentAuthSource,
- {{features::kOidcAuthAdditionalHosts.name, additional_hosts}}});
- }
+ {{features::kOidcAuthProfileManagement, enable_oidc_interception}});
scoped_feature_list_.InitWithFeaturesAndParameters(enabled_features,
disabled_features);
}
- OidcAuthResponseCaptureNavigationThrottleTest()
- : OidcAuthResponseCaptureNavigationThrottleTest(
- /*additional_hosts=*/std::string()) {}
-
- ~OidcAuthResponseCaptureNavigationThrottleTest() override = default;
-
void SetUpOnMainThread() override {
InProcessBrowserTest::SetUpOnMainThread();
- in_process_data_decoder_ =
- std::make_unique<data_decoder::test::InProcessDataDecoder>();
- test_ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
-
OidcAuthenticationSigninInterceptorFactory::GetInstance()
->SetTestingFactory(
browser()->profile(),
@@ -254,50 +156,6 @@
EXPECT_EQ(tokens.state, expected_tokens.state);
}
- void SetupRedirectionForHandle(
- content::MockNavigationHandle& navigation_handle,
- std::vector<GURL> source_urls,
- const GURL& last_url) {
- navigation_handle.set_url(last_url);
- navigation_handle.set_redirect_chain(source_urls);
- }
-
- void RunThrottleAndExpectNoOidcInterception(
- MockOidcAuthenticationSigninInterceptor* oidc_interceptor,
- const std::string& redirection_url,
- NavigationThrottle::ThrottleAction expected_throttle_action) {
- base::RunLoop run_loop;
-
- content::MockNavigationHandle navigation_handle(
- GURL(kOidcEntraReprocessUrl), main_frame());
-
- EXPECT_CALL(*oidc_interceptor,
- MaybeInterceptOidcAuthentication(_, _, _, _, _, _))
- .Times(0);
-
- content::MockNavigationThrottleRegistry registry(
- &navigation_handle,
- content::MockNavigationThrottleRegistry::RegistrationMode::kHold);
- OidcAuthResponseCaptureNavigationThrottle::MaybeCreateAndAdd(registry);
- ASSERT_EQ(1u, registry.throttles().size());
- auto* throttle = registry.throttles().back().get();
-
- if (expected_throttle_action == NavigationThrottle::DEFER) {
- throttle->set_resume_callback_for_testing(run_loop.QuitClosure());
- }
-
- SetupRedirectionForHandle(
- navigation_handle,
- {GURL(kOidcEntraReprocessUrl), GURL(redirection_url)},
- GURL(redirection_url));
- EXPECT_EQ(expected_throttle_action,
- throttle->WillRedirectRequest().action());
-
- if (expected_throttle_action == NavigationThrottle::DEFER) {
- run_loop.Run();
- }
- }
-
void ExpectOidcInterception(
MockOidcAuthenticationSigninInterceptor* oidc_interceptor,
ProfileManagementOidcTokens expected_oidc_tokens) {
@@ -323,8 +181,9 @@
auto test_web_content = content::WebContents::Create(
content::WebContents::CreateParams(invalid_profile));
content::MockNavigationHandle navigation_handle(test_web_content.get());
+ navigation_handle.set_url(GURL(kHeaderInterceptionTestUrl));
+ navigation_handle.set_response_headers(BuildExampleResponseHeader());
- navigation_handle.set_url(GURL(kOidcEntraReprocessUrl));
ASSERT_EQ(nullptr, oidc_interceptor);
content::MockNavigationThrottleRegistry registry(
@@ -332,80 +191,18 @@
content::MockNavigationThrottleRegistry::RegistrationMode::kHold);
OidcAuthResponseCaptureNavigationThrottle::MaybeCreateAndAdd(registry);
ASSERT_EQ(1u, registry.throttles().size());
-
- std::string redirection_url =
- BuildStandardResponseUrl(/*oidc_state=*/std::string());
- SetupRedirectionForHandle(
- navigation_handle,
- {GURL(kOidcEntraReprocessUrl), GURL(redirection_url)},
- GURL(redirection_url));
-
- EXPECT_EQ(NavigationThrottle::PROCEED,
- registry.throttles().back()->WillRedirectRequest().action());
-
- CheckFunnelAndResultHistogram(
- OidcInterceptionFunnelStep::kValidRedirectionCaptured,
- OidcInterceptionResult::kInvalidProfile);
- }
-
- void TestInterceptionForUrl(bool add_oidc_state,
- bool should_log_ukm,
- std::string source_url) {
- base::RunLoop run_loop;
- std::string auth_token = BuildTokenFromDict(
- base::DictValue()
- .Set(kUserPrincipleNameClaimName, kExampleUserPrincipleName)
- .Set(kSubjectClaimName, kExampleAuthSubject));
- std::string id_token = BuildTokenFromDict(
- base::DictValue()
- .Set(kUserPrincipleNameClaimName, kExampleUserPrincipleName)
- .Set(kSubjectClaimName, kExampleIdSubject)
- .Set(kIssuerClaimName, kExampleIdIssuer));
- std::string oidc_state =
- (enable_generic_oidc() && add_oidc_state) ? kOidcState : std::string();
-
- std::string redirection_url =
- BuildOidcResponseUrl(auth_token, id_token, oidc_state);
- content::MockNavigationHandle navigation_handle(GURL(source_url),
- main_frame());
-
- auto* oidc_interceptor = GetMockOidcInterceptor();
- if (enable_generic_oidc() || IsSourceUrlValid(source_url)) {
- ExpectOidcInterception(
- oidc_interceptor,
- ProfileManagementOidcTokens(auth_token, id_token, oidc_state));
- } else {
- EXPECT_CALL(*oidc_interceptor,
- MaybeInterceptOidcAuthentication(_, _, _, _, _, _))
- .Times(0);
- }
-
- content::MockNavigationThrottleRegistry registry(
- &navigation_handle,
- content::MockNavigationThrottleRegistry::RegistrationMode::kHold);
- OidcAuthResponseCaptureNavigationThrottle::MaybeCreateAndAdd(registry);
- ASSERT_EQ(1u, registry.throttles().size());
... (truncated)
Original Bug Report
Bypass of OIDC enrollment redirect-chain and missing JWT signature verification
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: The OIDC enterprise enrollment process in Chrome fails to verify the JWT signature locally and has flawed redirect-chain validation logic. This allows an attacker to proxy via trusted domains (like login.microsoftonline.com) and trigger a deceptive native ‘Your organization requires a new profile’ dialog. If accepted, the attacker gains enterprise policy control over a new Chrome profile.
Affected files:
chrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle.ccchrome/browser/enterprise/signin/oidc_authentication_signin_interceptor.ccchrome/browser/ui/signin/dice_web_signin_interceptor_delegate.cc
Estimated timestamp from git blame: 2026-01-16
Summary
The OIDC NavigationThrottle in Chrome is used to intercept navigations to https://chromeenterprise.google/enroll for initiating enterprise profile enrollment. There are three potential security flaws in its implementation that allow an attacker to bypass the trust validation for enrollment requests and trigger deceptive, native OS dialogs. If the user accepts the dialog, an attacker can gain control over a new Chrome profile via enterprise policies.
Vulnerability Details
1. Insecure Redirect-Chain Validation (Any Hop)
The throttle attempts to ensure that the enrollment request originates from a trusted source by checking the navigation’s redirect chain. However, at chrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle.cc:265-270, the code iterates through the entire redirect chain and accepts the navigation if any hop matches a trusted host:
for (const auto& chain_url : navigation_handle()->GetRedirectChain()) {
if (!GetOidcEnrollmentUrlMatcher()->MatchURL(chain_url).empty()) {
accept_redirect = true;
break;
}
}
An attacker can satisfy this check by registering an Azure AD application in their own tenant. A navigation that passes through login.microsoftonline.com (a trusted host) on its way to the enrollment URL will satisfy the check, regardless of the intermediate hops or the initiating untrusted domain.
2. Wildcard Host Matching for MCAS (Subdomain Bypass)
The host https://mcas.ms is included in the trusted list. Due to how AddAllowFiltersWithLimit and FilterToComponents (components/url_matcher/url_util.cc:387-395) process mcas.ms (evaluating it as url::CanonHostInfo::NEUTRAL), the code prepends a dot ("." + *host) and sets *match_subdomains = true. This results in a wildcard match for any *.mcas.ms subdomain. Microsoft Defender for Cloud Apps (MCAS) allows tenants to proxy applications through subdomains like <app>.<region>.mcas.ms, which an attacker can control. An attacker can use an MCAS-proxied application to trigger the enrollment redirect, bypassing the source domain restriction entirely.
3. Missing JWT Signature Verification
In chrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle.cc, the id_token JWT provided in the URL fragment is parsed without verifying its cryptographic signature. The code at lines 330-347 splits the token on . and only base64-decodes the payload (section 1). The signature (section 2) is entirely ignored. The browser relies on this unverified token payload to display a highly deceptive, native ‘Your organization requires a new profile’ dialog (DiceWebSigninInterceptorDelegate::ShowOidcInterceptionDialog).
Impact
By exploiting these bypasses, an attacker can programmatically trigger a native ‘Your organization requires a new profile’ modal in the browser UI. This dialog is shown before any server-side cryptographic validation takes place. Because the dialog is a native Chrome prompt, users are likely to accept it.
If the user clicks ‘Accept’ or ‘Continue’, a new managed profile is created. If the attacker has enrolled their Azure AD tenant in Chrome Enterprise Core, the valid token (minted by the attacker’s real tenant) is sent to the Google Device Management server. The attacker can then push arbitrary enterprise policies to this profile. This includes forced malicious extension installation, enterprise root CA injection, and proxy configurations, effectively giving the attacker total control over the user’s data and traffic within that new profile.
Suggested Exploitation Steps
(Note: These are potential steps based on code analysis; an end-to-end exploit has not been fully verified via execution.)
- Attacker Setup: Register an OAuth application in an Azure AD tenant controlled by the attacker with
redirect_uri=https://chromeenterprise.google/enrollandresponse_type=id_token token. - (Optional, for full impact): Enroll this tenant as a third-party IdP in Chrome Enterprise Core.
- Lure Victim: Host a malicious webpage (
https://attacker.com) and convince the victim to visit it. - Redirection: The webpage redirects the victim’s browser to the Azure AD authorization endpoint (
https://login.microsoftonline.com) for the attacker’s app. - Authentication & Redirection: If the victim has an active Microsoft session, Azure AD silently authenticates and redirects to
https://chromeenterprise.google/enroll#id_token=<attacker_jwt>&.... - Bypass Checks: The
OIDC NavigationThrottleaccepts the navigation becauselogin.microsoftonline.comis in the redirect chain (or because an attacker-controlled*.mcas.msproxy was used). - Deceptive Dialog: The native ‘Your organization requires a new profile’ dialog is displayed to the user, based on the unverified token payload.
- Compromise: Upon acceptance, the valid token is sent to the backend, the profile is created, and it is managed by the attacker’s enterprise policy.
Suggested Fixes
- Fix Redirect Chain Validation: Modify
oidc_auth_response_capture_navigation_throttle.cc:265-270to verify the entire redirect chain (or at least the initiating origin and all intermediate hops) against the trusted allowlist, rather than accepting the navigation if any single hop matches. - Fix MCAS Wildcard Matching: Re-evaluate the inclusion of
https://mcas.msin the allowlist. If specific subdomains are expected, enumerate them. Alternatively, if*.mcas.msis required, implement stricter validation of the originating request or the JWT itself to prevent abuse of attacker-controlled proxy subdomains. - Implement Local JWT Verification: Add cryptographic signature verification for the
id_tokeninOidcAuthResponseCaptureNavigationThrottle::AttemptToTriggerUrlInterceptionbefore displaying any UI prompts. The browser must independently verify that the token was signed by a trusted, enrolled Enterprise IdP.
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from 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.