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 Reporting
DescriptionInsufficient validation of untrusted input in Enterprise Reporting
ComponentEnterprise Reporting
Bug ClassLogic Error
Tracker501467566
Fix commitcbf75ba3221f (chromium/src) +55/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
chrome/browser/extensions/api/enterprise_reporting_private/chrome_desktop_report_request_helper.cc
modified
TEST_F
chrome/browser/extensions/api/enterprise_reporting_private/enterprise_reporting_private_unittest.cc
modified

Files Changed

  • chrome/browser/extensions/api/enterprise_reporting_private/chrome_desktop_report_request_helper.cc
  • chrome/browser/extensions/api/enterprise_reporting_private/enterprise_reporting_private_unittest.cc
From cbf75ba3221f5e3730b750295fabdadd2aed719a Mon Sep 17 00:00:00 2001
From: Sebastien Lalancette <seblalancette@chromium.org>
Date: Wed, 22 Apr 2026 09:52:57 -0700
Subject: [PATCH] Prevent Path Traversal via Get/Set Device Data APIs

Fixed: 501467566
Change-Id: Ifed18845a84a991d9ffe61745244546fae979b3b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7785193
Reviewed-by: Dominique Fauteux-Chapleau <domfc@chromium.org>
Auto-Submit: Sebastien Lalancette <seblalancette@chromium.org>
Commit-Queue: Sebastien Lalancette <seblalancette@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1618937}
---

diff --git a/chrome/browser/extensions/api/enterprise_reporting_private/chrome_desktop_report_request_helper.cc b/chrome/browser/extensions/api/enterprise_reporting_private/chrome_desktop_report_request_helper.cc
index 31520027..50ac033 100644
--- a/chrome/browser/extensions/api/enterprise_reporting_private/chrome_desktop_report_request_helper.cc
+++ b/chrome/browser/extensions/api/enterprise_reporting_private/chrome_desktop_report_request_helper.cc
@@ -295,6 +295,19 @@
   return path;
 }
 
+std::optional<base::FilePath> GetValidatedDeviceDataFilePath(
+    const base::FilePath& data_dir,
+    const std::string& id) {
+  if (data_dir.empty()) {
+    return std::nullopt;
+  }
+  base::FilePath target_file = data_dir.AppendASCII(id);
+  if (target_file.ReferencesParent() || !data_dir.IsParent(target_file)) {
+    return std::nullopt;
+  }
+  return target_file;
+}
+
 }  // namespace
 
 // Sets the path used to store Endpoint Verification data for tests.
@@ -311,9 +324,13 @@
     return;
   }
 
-  // TODO(pastarmovj): Make sure the resulting path is still a direct file or
-  // subdir+file of the EV folder.
-  data_file = data_file.AppendASCII(id);
+  std::optional<base::FilePath> target_file =
+      GetValidatedDeviceDataFilePath(data_file, id);
+  if (!target_file) {
+    std::move(callback).Run(false);
+    return;
+  }
+  data_file = *target_file;
 
   bool success = false;
   if (data) {
@@ -358,7 +375,14 @@
                             RetrieveDeviceDataStatus::kDataDirectoryUnknown);
     return;
   }
-  data_file = data_file.AppendASCII(id);
+
+  std::optional<base::FilePath> target_file =
+      GetValidatedDeviceDataFilePath(data_file, id);
+  if (!target_file) {
+    std::move(callback).Run("", RetrieveDeviceDataStatus::kDataRecordNotFound);
+    return;
+  }
+  data_file = *target_file;
   // If the file does not exist don't treat this as an error rather return an
   // empty string.
   if (!base::PathExists(data_file)) {
diff --git a/chrome/browser/extensions/api/enterprise_reporting_private/enterprise_reporting_private_unittest.cc b/chrome/browser/extensions/api/enterprise_reporting_private/enterprise_reporting_private_unittest.cc
index c15224e..1aad2c76 100644
--- a/chrome/browser/extensions/api/enterprise_reporting_private/enterprise_reporting_private_unittest.cc
+++ b/chrome/browser/extensions/api/enterprise_reporting_private/enterprise_reporting_private_unittest.cc
@@ -221,6 +221,33 @@
   EXPECT_FALSE(function->GetError().empty());
 }
 
+TEST_F(EnterpriseReportingPrivateDeviceDataFunctionsTest, DevicePathTraversal) {
+  auto set_function =
+      base::MakeRefCounted<EnterpriseReportingPrivateSetDeviceDataFunction>();
+  base::ListValue set_values;
+  set_values.Append("../traversal");
+  set_values.Append(base::Value::BlobStorage({1, 2, 3}));
+  api_test_utils::RunFunction(set_function.get(), std::move(set_values),
+                              profile(),
+                              extensions::api_test_utils::FunctionMode::kNone);
+  // It should fail and set an error.
+  EXPECT_FALSE(set_function->GetError().empty());
+
+  auto get_function =
+      base::MakeRefCounted<EnterpriseReportingPrivateGetDeviceDataFunction>();
+  base::ListValue get_values;
+  get_values.Append("../traversal");
+  api_test_utils::RunFunction(get_function.get(), std::move(get_values),
+                              profile(),
+                              extensions::api_test_utils::FunctionMode::kNone);
+  // It should also fail or return empty blob. Based on our implementation,
+  // it returns kDataRecordNotFound which translates to empty blob in API.
+  ASSERT_TRUE(get_function->GetResultListForTest());
+  const base::Value& single_result = (*get_function->GetResultListForTest())[0];
+  ASSERT_TRUE(single_result.is_blob());
+  EXPECT_EQ(base::Value::BlobStorage(), single_result.GetBlob());
+}
+
 TEST_F(EnterpriseReportingPrivateDeviceDataFunctionsTest, RetrieveDeviceData) {
   auto set_function =
       base::MakeRefCounted<EnterpriseReportingPrivateSetDeviceDataFunction>();
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/extensions/api/enterprise_reporting_private/enterprise_reporting_private_unittest.cc b/chrome/browser/extensions/api/enterprise_reporting_private/enterprise_reporting_private_unittest.cc
index c15224e..1aad2c76 100644
--- a/chrome/browser/extensions/api/enterprise_reporting_private/enterprise_reporting_private_unittest.cc
+++ b/chrome/browser/extensions/api/enterprise_reporting_private/enterprise_reporting_private_unittest.cc
@@ -221,6 +221,33 @@
   EXPECT_FALSE(function->GetError().empty());
 }
 
+TEST_F(EnterpriseReportingPrivateDeviceDataFunctionsTest, DevicePathTraversal) {
+  auto set_function =
+      base::MakeRefCounted<EnterpriseReportingPrivateSetDeviceDataFunction>();
+  base::ListValue set_values;
+  set_values.Append("../traversal");
+  set_values.Append(base::Value::BlobStorage({1, 2, 3}));
+  api_test_utils::RunFunction(set_function.get(), std::move(set_values),
+                              profile(),
+                              extensions::api_test_utils::FunctionMode::kNone);
+  // It should fail and set an error.
+  EXPECT_FALSE(set_function->GetError().empty());
+
+  auto get_function =
+      base::MakeRefCounted<EnterpriseReportingPrivateGetDeviceDataFunction>();
+  base::ListValue get_values;
+  get_values.Append("../traversal");
+  api_test_utils::RunFunction(get_function.get(), std::move(get_values),
+                              profile(),
+                              extensions::api_test_utils::FunctionMode::kNone);
+  // It should also fail or return empty blob. Based on our implementation,
+  // it returns kDataRecordNotFound which translates to empty blob in API.
+  ASSERT_TRUE(get_function->GetResultListForTest());
+  const base::Value& single_result = (*get_function->GetResultListForTest())[0];
+  ASSERT_TRUE(single_result.is_blob());
+  EXPECT_EQ(base::Value::BlobStorage(), single_result.GetBlob());
+}
+
 TEST_F(EnterpriseReportingPrivateDeviceDataFunctionsTest, RetrieveDeviceData) {
   auto set_function =
       base::MakeRefCounted<EnterpriseReportingPrivateSetDeviceDataFunction>();
Loading diff…

Original Bug Report

reported by vm...@google.com

Path Traversal in enterprise.reportingPrivate.setDeviceData Allows Arbitrary File Deletion

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.

Overview: The enterprise.reportingPrivate.setDeviceData extension API does not sanitize the id parameter against path traversal. A compromised extension from the allowed list can exploit this to delete arbitrary files, create arbitrary directories, and drop files containing attacker-controlled data anywhere the user has write access.

Affected files:

  • chrome/browser/extensions/api/enterprise_reporting_private/chrome_desktop_report_request_helper.cc
  • chrome/browser/extensions/api/enterprise_reporting_private/enterprise_reporting_private_api.cc

Estimated timestamp from git blame: 2026-02-25

Vulnerability Details

The enterprise.reportingPrivate.setDeviceData extension API allows a specific allowlist of enterprise extensions (such as Endpoint Verification) to store data on disk. This API takes a DOMString id and an optional ArrayBuffer data.

When a request is received, the extension arguments are passed to the browser process and handled by StoreDeviceData in chrome/browser/extensions/api/enterprise_reporting_private/chrome_desktop_report_request_helper.cc.

void StoreDeviceData(const std::string& id,
                     const std::optional<std::vector<uint8_t>> data,
                     base::OnceCallback<void(bool)> callback) {
  base::FilePath data_file = GetEndpointVerificationDir();
  // ...

  // TODO(pastarmovj): Make sure the resulting path is still a direct file or
  // subdir+file of the EV folder.
  data_file = data_file.AppendASCII(id);

The id parameter provided by the renderer is directly appended to the base directory path using base::FilePath::AppendASCII. Because AppendASCII does not sanitize path traversal sequences like .., the resulting data_file path can escape the intended Endpoint Verification directory, granting the attacker control over the target file path on the local filesystem.

Impact

A compromised privileged renderer can leverage this traversal to achieve the following primitives:

  1. Arbitrary File Deletion: If the API is called with an empty or omitted data argument, the code attempts to clear existing data by executing:

    success = base::DeleteFile(data_file);
    if (base::IsDirectoryEmpty(data_file.DirName())) {
      base::DeleteFile(data_file.DirName());
    }
    

    Since base::DeleteFile lacks path traversal checks, this allows the deletion of arbitrary files accessible to the browser process (the user’s privileges). Furthermore, if deleting the file leaves its parent directory empty, that parent directory is also deleted.

  2. Arbitrary Directory Creation: If data is provided, the function executes:

    success = base::CreateDirectory(data_file.DirName());
    

    This enables the creation of arbitrary directory structures on the filesystem.

  3. Arbitrary File Drop: When data is provided, the code creates a temporary file in the attacker-controlled target directory and writes the attacker’s payload to it. It then attempts to move this file to the final destination using base::Move.

    base::CreateTemporaryFileInDir(data_file.DirName(), &tmp_path);
    base::WriteFile(tmp_path, *data);
    success = base::Move(tmp_path, data_file);
    

    While base::Move successfully blocks the move operation because it contains an internal check against .., the implementation does not clean up the temporary file upon failure. This leaves a file with a random name (e.g., .[branding].XXXXXX on POSIX systems) containing attacker-controlled content permanently on disk in the targeted directory.

Potential Exploitation Steps

(Note: These are theoretical steps; a functional proof-of-concept has not been run.)

  1. An attacker compromises the renderer process hosting an extension allowed to use the enterprise.reportingPrivate API (e.g., Endpoint Verification, 031E5E4A54C39E4F46E11CE643584E9187915908).
  2. To delete a file, the attacker invokes the API via JavaScript: chrome.enterprise.reportingPrivate.setDeviceData({id: "../../../../../../etc/passwd"}). The browser process parses the missing data parameter as nullopt and deletes the targeted file.
  3. To drop a file, the attacker invokes: chrome.enterprise.reportingPrivate.setDeviceData({id: "../../../../../../tmp/attacker_dir/target", data: new Uint8Array([0x41, 0x41])}). The browser creates /tmp/attacker_dir/ and drops a randomly named file containing AA inside it.

Suggested Fix

Ensure that the id parameter does not contain path traversal sequences before appending it to the directory path. The existing TODO should be addressed by verifying that the resulting path is a direct child of the Endpoint Verification directory.

  data_file = data_file.AppendASCII(id);
  if (data_file.ReferencesParent()) {
    std::move(callback).Run(false);
    return;
  }

Alternatively, id could be sanitized or validated to only contain alphanumeric characters.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


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 1163683

View on issue tracker