Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Enterprise
DescriptionInsufficient validation of untrusted input in Enterprise
ComponentEnterprise
Bug ClassLogic Error
Tracker512162479
Fix commit3c576832ba79 (chromium/src) +46/-1043
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
for
chrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle.cc
modified
if
chrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle.cc
modified
WillRedirectRequest
chrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle.cc
modified
WillProcessResponse
chrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle.cc
modified
GetOidcEnrollmentUrlMatcherForTesting
chrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle.cc
modified
AttemptToTriggerUrlInterception
chrome/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
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;
Loading diff…

Regression Test / PoC

shipped with the fix
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)
Loading diff…

Original Bug Report

reported by [email protected]

Zero-click OIDC profile switch and enterprise policy bypass via unverified JWT

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: Chrome’s OIDC enrollment flow extracts identity claims from URL fragments without validating the JWT signature. An attacker can use a crafted redirect from a trusted identity provider to force a zero-click profile switch to an existing managed profile using spoofed claims. This process temporarily wipes local enterprise policies from the target profile, creating an unmanaged window.

Affected files:

  • chrome/browser/enterprise/profile_management/oidc_auth_response_capture_navigation_throttle.cc
  • chrome/browser/enterprise/signin/oidc_authentication_signin_interceptor.cc
  • chrome/browser/ui/signin/dice_web_signin_interceptor_delegate.cc

Estimated timestamp from git blame: 2026-01-16

Background

Chrome supports OpenID Connect (OIDC) profile enrollment, which captures identity tokens (JWTs) from URL fragments at specific endpoints like https://chromeenterprise.google/enroll after a redirect from a trusted identity provider (IDP) such as Microsoft Entra ID.

Vulnerability Details

There is a potential vulnerability in OidcAuthResponseCaptureNavigationThrottle where the id_token is parsed from the URL fragment but its cryptographic signature is never verified. The throttle base64url-decodes the payload and extracts the iss (issuer) and sub (subject) claims, trusting them implicitly.

These unverified claims are passed to OidcAuthenticationSigninInterceptor::MaybeInterceptOidcAuthentication, which uses them to look up existing profiles. If a match is found, it triggers a kProfileSwitchForced interception. DiceWebSigninInterceptorDelegate handles this forced switch by creating a ForcedProfileSwitchInterceptionHandle, which automatically posts a task to accept the switch without displaying any user consent bubble or UI.

Upon switching to the target profile, OnNewSignedInProfileCreated invokes UserPolicyOidcSigninService::ResetGaiaPolicyManagement(). This initiates a shutdown of the cloud policy manager and calls DesktopCloudPolicyStore::Clear(), which explicitly deletes the enterprise policy and key files from disk and clears the in-memory cache. While Chrome subsequently attempts to fetch new policies, this creates a window where the profile operates entirely unmanaged. If the attacker blocks the network fetch or provides an invalid token, the profile remains permanently unmanaged.

Suggested Reproduction Steps

Note: Our tooling cannot run live code, so these are suggested steps based on static code analysis.

Preconditions: The attacker must know the victim’s OIDC iss and sub identifiers.

  1. The attacker registers a standard Microsoft Entra ID (Azure AD) application and sets the redirect_uri to https://chromeenterprise.google/enroll.
  2. The attacker crafts a Base64URL-encoded JWT (<SPOOFED_JWT>) containing the victim’s iss and sub claims, but without a valid signature.
  3. The attacker creates an authorization URL for their Azure AD app and appends the spoofed token in the fragment: https://login.microsoftonline.com/.../authorize?client_id=<attacker_client>&redirect_uri=https://chromeenterprise.google/enroll#id_token=<SPOOFED_JWT>.
  4. The victim is lured to an attacker-controlled page which triggers a top-level drive-by navigation to this URL.
  5. Microsoft processes the request and issues a 302 Found redirect to the registered URI. The browser automatically preserves the URL fragment across the redirect.
  6. Chrome intercepts the navigation to chromeenterprise.google/enroll. The redirect chain check passes because the redirect originated from the trusted login.microsoftonline.com host.
  7. Chrome extracts the unverified JWT, matches the victim’s profile, silently forces a profile switch, and wipes the target profile’s enterprise policies.

Suggested Fix

  1. Implement strict cryptographic signature validation for the id_token JWT in OidcAuthResponseCaptureNavigationThrottle before extracting and trusting any claims.
  2. Re-evaluate the kProfileSwitchForced zero-click logic to ensure it cannot be abused to disrupt an existing managed session.

Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955


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.

View on issue tracker