CVE-2026-11120
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/extensions/api/enterprise_reporting_private/chrome_desktop_report_request_helper.cc |
modified | |
TEST_Fchrome/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.ccchrome/browser/extensions/api/enterprise_reporting_private/enterprise_reporting_private_unittest.cc
Patch
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>();
Regression Test / PoC
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>();
Original Bug Report
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.ccchrome/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:
-
Arbitrary File Deletion: If the API is called with an empty or omitted
dataargument, 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::DeleteFilelacks 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. -
Arbitrary Directory Creation: If
datais provided, the function executes:success = base::CreateDirectory(data_file.DirName());This enables the creation of arbitrary directory structures on the filesystem.
-
Arbitrary File Drop: When
datais 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 usingbase::Move.base::CreateTemporaryFileInDir(data_file.DirName(), &tmp_path); base::WriteFile(tmp_path, *data); success = base::Move(tmp_path, data_file);While
base::Movesuccessfully 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].XXXXXXon 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.)
- An attacker compromises the renderer process hosting an extension allowed to use the
enterprise.reportingPrivateAPI (e.g., Endpoint Verification,031E5E4A54C39E4F46E11CE643584E9187915908). - To delete a file, the attacker invokes the API via JavaScript:
chrome.enterprise.reportingPrivate.setDeviceData({id: "../../../../../../etc/passwd"}). The browser process parses the missingdataparameter asnulloptand deletes the targeted file. - 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 containingAAinside 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