Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactCryptographic Flaw in Enterprise
DescriptionCryptographic Flaw in Enterprise
ComponentEnterprise
Bug ClassLogic Error
Tracker511761758
Fix commite469631fc473 (chromium/src) +92/-9
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
TEST_F
components/policy/core/common/cloud/cloud_policy_validator_unittest.cc
modified

Files Changed

  • components/policy/core/common/cloud/cloud_policy_validator.cc
  • components/policy/core/common/cloud/cloud_policy_validator.h
  • components/policy/core/common/cloud/cloud_policy_validator_unittest.cc
  • components/policy/core/common/features.cc
From e469631fc473ce56294ba348a7aa88aa871d5838 Mon Sep 17 00:00:00 2001
From: Nicolas Ouellet-Payeur <nicolaso@chromium.org>
Date: Thu, 02 Jul 2026 11:18:09 -0700
Subject: [PATCH] policy: Validate public key matches verification data

Check that the public key certified by `new_public_key_verification_data`
equals the actual `new_public_key` in `CloudPolicyValidator`.

Bug: 511761758
Change-Id: I40e2423fb81b59336ee0a93484a694186a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8020683
Commit-Queue: Nicolas Ouellet-Payeur <nicolaso@chromium.org>
Reviewed-by: Owen Min <zmin@chromium.org>
Reviewed-by: Igor <igorcov@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1656181}
---

diff --git a/components/policy/core/common/cloud/cloud_policy_validator.cc b/components/policy/core/common/cloud/cloud_policy_validator.cc
index 6a5e1a6..1591f71d 100644
--- a/components/policy/core/common/cloud/cloud_policy_validator.cc
+++ b/components/policy/core/common/cloud/cloud_policy_validator.cc
@@ -13,6 +13,7 @@
 #include "base/base64.h"
 #include "base/check_is_test.h"
 #include "base/command_line.h"
+#include "base/feature_list.h"
 #include "base/functional/callback_helpers.h"
 #include "base/location.h"
 #include "base/logging.h"
@@ -23,6 +24,7 @@
 #include "cloud_policy_validator.h"
 #include "components/policy/core/common/cloud/cloud_policy_constants.h"
 #include "components/policy/core/common/cloud/cloud_policy_util.h"
+#include "components/policy/core/common/features.h"
 #include "components/policy/core/common/policy_logger.h"
 #include "components/policy/core/common/policy_switches.h"
 #include "components/policy/proto/device_management_backend.pb.h"
@@ -454,8 +456,9 @@
                       verification_key_.value(),
                       policy_->new_public_key_verification_data_signature(),
                       em::PolicyFetchRequest::SHA256_RSA) &&
-      CheckDomainInPublicKeyVerificationData(
-          policy_->new_public_key_verification_data())) {
+      CheckPublicKeyVerificationData(
+          policy_->new_public_key_verification_data(),
+          policy_->new_public_key())) {
     UMA_HISTOGRAM_ENUMERATION(kMetricKeySignatureVerification,
                               MetricKeySignatureVerification::kSuccess);
     // Signature verification succeeded - return success to the caller.
@@ -542,8 +545,9 @@
   return domain;
 }
 
-bool CloudPolicyValidatorBase::CheckDomainInPublicKeyVerificationData(
-    const std::string& new_public_key_verification_data) {
+bool CloudPolicyValidatorBase::CheckPublicKeyVerificationData(
+    const std::string& new_public_key_verification_data,
+    const std::string& expected_public_key) {
   em::PublicKeyVerificationData public_key_data;
   if (!public_key_data.ParseFromString(new_public_key_verification_data)) {
     LOG_POLICY(ERROR, POLICY_FETCHING)
@@ -551,6 +555,13 @@
         << "Failed to deserialize new public key.";
     return false;
   }
+  if (base::FeatureList::IsEnabled(features::kVerifyVerificationDataKey) &&
+      public_key_data.new_public_key() != expected_public_key) {
+    LOG_POLICY(ERROR, POLICY_FETCHING)
+        << PolicyTypeLogPrefix(policy_type_, settings_entity_id_)
+        << "Key mismatch in new public key verification data.";
+    return false;
+  }
   if (public_key_data.domain() != ExtractDomainFromPolicy()) {
     LOG_POLICY(ERROR, POLICY_FETCHING)
         << PolicyTypeLogPrefix(policy_type_, settings_entity_id_)
@@ -631,7 +642,7 @@
   if (VerifySignature(new_cached_key_, verification_key_.value(),
                       new_cached_key_signature_,
                       em::PolicyFetchRequest::SHA256_RSA) &&
-      CheckDomainInPublicKeyVerificationData(new_cached_key_)) {
+      CheckPublicKeyVerificationData(new_cached_key_, cached_key_)) {
     UMA_HISTOGRAM_ENUMERATION(kMetricKeySignatureVerification,
                               MetricKeySignatureVerification::kSuccess);
     // Signature verification succeeded - return success to the caller.
diff --git a/components/policy/core/common/cloud/cloud_policy_validator.h b/components/policy/core/common/cloud/cloud_policy_validator.h
index fabba76..653528a 100644
--- a/components/policy/core/common/cloud/cloud_policy_validator.h
+++ b/components/policy/core/common/cloud/cloud_policy_validator.h
@@ -354,10 +354,12 @@
   // empty string if the policy does not contain a username field.
   std::string ExtractDomainFromPolicy();
 
-  // Returns if the domain from the new_public_key_verification_data matches
-  // the domain extracted from the |policy_|.
-  bool CheckDomainInPublicKeyVerificationData(
-      const std::string& new_public_key_verification_data);
+  // Returns true if |new_public_key_verification_data| can be parsed, the
+  // public key it certifies equals |expected_public_key| and its domain
+  // matches the domain extracted from |policy_|.
+  bool CheckPublicKeyVerificationData(
+      const std::string& new_public_key_verification_data,
+      const std::string& expected_public_key);
 
   // Sets the owning domain used to verify new public keys, and ensures that
   // callers don't try to set conflicting values.
diff --git a/components/policy/core/common/cloud/cloud_policy_validator_unittest.cc b/components/policy/core/common/cloud/cloud_policy_validator_unittest.cc
index 8cc7185..924a6d5 100644
--- a/components/policy/core/common/cloud/cloud_policy_validator_unittest.cc
+++ b/components/policy/core/common/cloud/cloud_policy_validator_unittest.cc
@@ -15,11 +15,13 @@
 #include "base/run_loop.h"
 #include "base/strings/string_util.h"
 #include "base/task/single_thread_task_runner.h"
+#include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
 #include "build/build_config.h"
 #include "cloud_policy_constants.h"
 #include "components/policy/core/common/cloud/cloud_policy_constants.h"
 #include "components/policy/core/common/cloud/test/policy_builder.h"
+#include "components/policy/core/common/features.h"
 #include "components/policy/core/common/policy_switches.h"
 #include "components/policy/proto/device_management_backend.pb.h"
 #include "google_apis/gaia/gaia_id.h"
@@ -544,6 +546,68 @@
                  policy_.GetCopy());
 }
 
+TEST_F(CloudPolicyValidatorTest, ErrorVerificationDataKeyMismatch) {
+  // Build a response whose new_public_key_verification_data certifies the
+  // default new signing key.
+  policy_.Build();
+  const std::string verification_data =
+      policy_.policy().new_public_key_verification_data();
+  const std::string verification_data_signature =
+      policy_.policy().new_public_key_verification_data_signature();
+
+  // Build a second response that delivers an unrelated new_public_key and
+  // signs policy_data with it, then attach the verification data from the
+  // first response.
+  UserPolicyBuilder other;
+  other.SetDefaultInitialSigningKey();
+  other.Build();
+  ASSERT_NE(other.policy().new_public_key(), policy_.policy().new_public_key());
+  other.policy().set_new_public_key_verification_data(verification_data);
+  other.policy().set_new_public_key_verification_data_signature(
+      verification_data_signature);
+  other.policy().clear_new_public_key_verification_signature_deprecated();
+
+  auto validator = std::make_unique<UserCloudPolicyValidator>(
+      other.GetCopy(), base::SingleThreadTaskRunner::GetCurrentDefault());
+  validator->ValidateInitialKey(PolicyBuilder::kFakeDomain);
+  validator->RunValidation();
+  EXPECT_EQ(CloudPolicyValidatorBase::VALIDATION_BAD_KEY_VERIFICATION_SIGNATURE,
+            validator->status());
+}
+
+TEST_F(CloudPolicyValidatorTest,
+       VerificationDataKeyMismatchIgnoredWhenFeatureDisabled) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  scoped_feature_list.InitAndDisableFeature(
+      features::kVerifyVerificationDataKey);
+
+  // Build a response whose new_public_key_verification_data certifies the
+  // default new signing key.
+  policy_.Build();
+  const std::string verification_data =
+      policy_.policy().new_public_key_verification_data();
+  const std::string verification_data_signature =
+      policy_.policy().new_public_key_verification_data_signature();
+
+  // Build a second response that delivers an unrelated new_public_key and
+  // signs policy_data with it, then attach the verification data from the
+  // first response.
+  UserPolicyBuilder other;
+  other.SetDefaultInitialSigningKey();
+  other.Build();
+  ASSERT_NE(other.policy().new_public_key(), policy_.policy().new_public_key());
+  other.policy().set_new_public_key_verification_data(verification_data);
+  other.policy().set_new_public_key_verification_data_signature(
+      verification_data_signature);
+  other.policy().clear_new_public_key_verification_signature_deprecated();
+
+  auto validator = std::make_unique<UserCloudPolicyValidator>(
+      other.GetCopy(), base::SingleThreadTaskRunner::GetCurrentDefault());
+  validator->ValidateInitialKey(PolicyBuilder::kFakeDomain);
+  validator->RunValidation();
+  EXPECT_EQ(CloudPolicyValidatorBase::VALIDATION_OK, validator->status());
+}
+
 TEST_F(CloudPolicyValidatorTest, ErrorDomainMismatchForKeyVerification) {
   policy_.Build();
   policy_.policy().set_new_public_key_verification_data("invalid");
diff --git a/components/policy/core/common/features.cc b/components/policy/core/common/features.cc
index 3e34abf..758199f 100644
--- a/components/policy/core/common/features.cc
+++ b/components/policy/core/common/features.cc
@@ -92,4 +92,6 @@
 BASE_FEATURE(kURLBlocklistOverridesIncognitoAllowlist,
              base::FEATURE_ENABLED_BY_DEFAULT);
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/policy/core/common/cloud/cloud_policy_validator_unittest.cc b/components/policy/core/common/cloud/cloud_policy_validator_unittest.cc
index 8cc7185..924a6d5 100644
--- a/components/policy/core/common/cloud/cloud_policy_validator_unittest.cc
+++ b/components/policy/core/common/cloud/cloud_policy_validator_unittest.cc
@@ -15,11 +15,13 @@
 #include "base/run_loop.h"
 #include "base/strings/string_util.h"
 #include "base/task/single_thread_task_runner.h"
+#include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
 #include "build/build_config.h"
 #include "cloud_policy_constants.h"
 #include "components/policy/core/common/cloud/cloud_policy_constants.h"
 #include "components/policy/core/common/cloud/test/policy_builder.h"
+#include "components/policy/core/common/features.h"
 #include "components/policy/core/common/policy_switches.h"
 #include "components/policy/proto/device_management_backend.pb.h"
 #include "google_apis/gaia/gaia_id.h"
@@ -544,6 +546,68 @@
                  policy_.GetCopy());
 }
 
+TEST_F(CloudPolicyValidatorTest, ErrorVerificationDataKeyMismatch) {
+  // Build a response whose new_public_key_verification_data certifies the
+  // default new signing key.
+  policy_.Build();
+  const std::string verification_data =
+      policy_.policy().new_public_key_verification_data();
+  const std::string verification_data_signature =
+      policy_.policy().new_public_key_verification_data_signature();
+
+  // Build a second response that delivers an unrelated new_public_key and
+  // signs policy_data with it, then attach the verification data from the
+  // first response.
+  UserPolicyBuilder other;
+  other.SetDefaultInitialSigningKey();
+  other.Build();
+  ASSERT_NE(other.policy().new_public_key(), policy_.policy().new_public_key());
+  other.policy().set_new_public_key_verification_data(verification_data);
+  other.policy().set_new_public_key_verification_data_signature(
+      verification_data_signature);
+  other.policy().clear_new_public_key_verification_signature_deprecated();
+
+  auto validator = std::make_unique<UserCloudPolicyValidator>(
+      other.GetCopy(), base::SingleThreadTaskRunner::GetCurrentDefault());
+  validator->ValidateInitialKey(PolicyBuilder::kFakeDomain);
+  validator->RunValidation();
+  EXPECT_EQ(CloudPolicyValidatorBase::VALIDATION_BAD_KEY_VERIFICATION_SIGNATURE,
+            validator->status());
+}
+
+TEST_F(CloudPolicyValidatorTest,
+       VerificationDataKeyMismatchIgnoredWhenFeatureDisabled) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  scoped_feature_list.InitAndDisableFeature(
+      features::kVerifyVerificationDataKey);
+
+  // Build a response whose new_public_key_verification_data certifies the
+  // default new signing key.
+  policy_.Build();
+  const std::string verification_data =
+      policy_.policy().new_public_key_verification_data();
+  const std::string verification_data_signature =
+      policy_.policy().new_public_key_verification_data_signature();
+
+  // Build a second response that delivers an unrelated new_public_key and
+  // signs policy_data with it, then attach the verification data from the
+  // first response.
+  UserPolicyBuilder other;
+  other.SetDefaultInitialSigningKey();
+  other.Build();
+  ASSERT_NE(other.policy().new_public_key(), policy_.policy().new_public_key());
+  other.policy().set_new_public_key_verification_data(verification_data);
+  other.policy().set_new_public_key_verification_data_signature(
+      verification_data_signature);
+  other.policy().clear_new_public_key_verification_signature_deprecated();
+
+  auto validator = std::make_unique<UserCloudPolicyValidator>(
+      other.GetCopy(), base::SingleThreadTaskRunner::GetCurrentDefault());
+  validator->ValidateInitialKey(PolicyBuilder::kFakeDomain);
+  validator->RunValidation();
+  EXPECT_EQ(CloudPolicyValidatorBase::VALIDATION_OK, validator->status());
+}
+
 TEST_F(CloudPolicyValidatorTest, ErrorDomainMismatchForKeyVerification) {
   policy_.Build();
   policy_.policy().set_new_public_key_verification_data("invalid");
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential missing key binding in CloudPolicyValidatorBase allows arbitrary policy injection via MITM

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: CloudPolicyValidatorBase correctly verifies the Google signature on a policy key verification blob but fails to verify that the key contained inside the blob matches the key used to sign the policy payload. This allows an attacker with TLS MITM capabilities to intercept initial policy fetches, substitute their own signing key, and persistently inject arbitrary enterprise policies into the device or profile.

Affected files:

  • components/policy/core/common/cloud/cloud_policy_validator.cc
  • components/policy/core/common/cloud/user_cloud_policy_store.cc
  • chrome/browser/ash/policy/enrollment/enrollment_handler.cc
  • chrome/browser/ash/policy/core/user_cloud_policy_store_ash.cc
  • chrome/browser/ash/policy/core/device_cloud_policy_store_ash.cc
  • chrome/enterprise_companion/dm_client.cc

Estimated timestamp from git blame: 2026-02-06

Summary

A potential vulnerability in CloudPolicyValidatorBase allows an attacker with a TLS Man-in-the-Middle (MITM) position to bypass the verification mechanism intended to protect initial policy delivery from the Device Management (DM) server.

The validator correctly checks the Google-signed PublicKeyVerificationData blob to ensure it is authentic and matches the victim’s domain. However, it fails to verify that the public key encapsulated inside this authenticated blob matches the new_public_key field in the PolicyFetchResponse (the key actually used to verify the policy payload). This missing cryptographic binding allows an attacker to substitute their own public key into the new_public_key field while providing a valid, captured Google-signed blob for the target domain.

Technical Details

In components/policy/core/common/cloud/cloud_policy_validator.cc, the CheckInitialKey() function uses policy_->new_public_key() to cryptographically verify the signature over the policy payload.

To ensure this key is trusted, it calls CheckNewPublicKeyVerificationSignature(), which verifies the Google signature over policy_->new_public_key_verification_data() using the hardcoded verification key. It then calls CheckDomainInPublicKeyVerificationData() to parse this blob and ensure its domain matches the domain of the device.

// components/policy/core/common/cloud/cloud_policy_validator.cc:545
bool CloudPolicyValidatorBase::CheckDomainInPublicKeyVerificationData(
    const std::string& new_public_key_verification_data) {
  em::PublicKeyVerificationData public_key_data;
  if (!public_key_data.ParseFromString(new_public_key_verification_data)) {
      // ...
  }
  if (public_key_data.domain() != ExtractDomainFromPolicy()) {
      // ...
  }
  return true;
}

The vulnerability is that public_key_data.new_public_key() (the authenticated key) is never compared against policy_->new_public_key() (the unauthenticated key used to verify the payload). Because CheckInitialKey() relies on policy_->new_public_key(), an attacker can arbitrarily replace it.

Upon successful validation, UserCloudPolicyStore::StoreLoaded caches and persists validator->policy()->new_public_key() (the attacker’s key) to disk. The attacker permanently controls future policy updates for the profile.

Potential Attack Scenario

An attacker requires a TLS MITM capability (e.g., via a compromised enterprise network or local trust anchor where public key pinning is bypassed).

  1. The attacker passively intercepts a legitimate policy fetch for the target domain (e.g., target.com) and extracts the new_public_key_verification_data blob and its Google signature.
  2. The attacker generates a malicious RSA keypair (K_evil).
  3. The attacker creates a malicious PolicyData payload (e.g., force-installing a malicious extension for UXSS/RCE capabilities).
  4. The victim initiates a fresh profile sign-in or device enrollment.
  5. The attacker intercepts the victim’s request, extracting the dm_token and device_id to populate their malicious payload.
  6. The attacker crafts a malicious PolicyFetchResponse where:
    • policy_data = the malicious payload.
    • new_public_key = the public key of K_evil.
    • policy_data_signature = the payload signed with the private key of K_evil.
    • new_public_key_verification_data and its signature are the ones captured in step 1.
  7. The victim’s browser validates the response. The payload signature is mathematically valid for K_evil. The Google signature is mathematically valid for the captured blob, and the domain matches. The policy is applied, and K_evil is persisted.

Fix Recommendation

The CheckDomainInPublicKeyVerificationData function (or a new dedicated check in CheckNewPublicKeyVerificationSignature) must verify that the key enclosed within the authenticated blob exactly matches the key being validated.

  if (public_key_data.has_new_public_key() && policy_->has_new_public_key() &&
      public_key_data.new_public_key() != policy_->new_public_key()) {
    LOG_POLICY(ERROR, POLICY_FETCHING)
        << PolicyTypeLogPrefix(policy_type_, settings_entity_id_)
        << "Key mismatch between policy and verification data.";
    return false;
  }

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.

Raised in root component due to access or custom field issues on 1227364

View on issue tracker