Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Feedback
DescriptionInsufficient validation of untrusted input in Feedback
ComponentFeedback
Bug ClassLogic Error
Tracker502248774
Fix commit30d636a452df (chromium/src) +47/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-04-28

Changed Functions

FunctionChangeNotes
TEST_F
components/feedback/feedback_util_unittest.cc
modified

Files Changed

  • components/feedback/feedback_util.cc
  • components/feedback/feedback_util_unittest.cc
From 30d636a452dfa0076ffdfe2826c358b3744bf902 Mon Sep 17 00:00:00 2001
From: Mike West <[email protected]>
Date: Tue, 14 Apr 2026 12:52:43 -0700
Subject: [PATCH] Prevent path traversal in ZipString.

This change uses base::SafeBaseName to ensure that the filename passed
to feedback_util::ZipString is a safe, single-component base name.
This prevents a potential path traversal vulnerability where a
compromised renderer could attempt to write files outside of the
temporary directory.

Bug: 502248774
Change-Id: I948b3ce274a2469ab30e0fb043473b6acfeffdfc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7758483
Reviewed-by: Xiangdong Kong <[email protected]>
Commit-Queue: Mike West <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1614676}
---

diff --git a/components/feedback/feedback_util.cc b/components/feedback/feedback_util.cc
index 767389e..5787f98 100644
--- a/components/feedback/feedback_util.cc
+++ b/components/feedback/feedback_util.cc
@@ -10,6 +10,7 @@
 
 #include "base/compiler_specific.h"
 #include "base/files/file_util.h"
+#include "base/files/safe_base_name.h"
 #include "base/files/scoped_temp_dir.h"
 #include "base/functional/bind.h"
 #include "base/json/json_reader.h"
@@ -32,6 +33,12 @@
 
 std::optional<std::string> ZipString(const base::FilePath& filename,
                                      std::string_view data) {
+  std::optional<base::SafeBaseName> safe_name =
+      base::SafeBaseName::Create(filename);
+  if (!safe_name || safe_name->path() != filename) {
+    return std::nullopt;
+  }
+
   base::ScopedTempDir temp_dir;
   base::FilePath zip_file;
 
@@ -40,7 +47,7 @@
   if (!temp_dir.CreateUniqueTempDir()) {
     return std::nullopt;
   }
-  if (!base::WriteFile(temp_dir.GetPath().Append(filename), data)) {
+  if (!base::WriteFile(temp_dir.GetPath().Append(safe_name->path()), data)) {
     return std::nullopt;
   }
   if (!base::CreateTemporaryFile(&zip_file)) {
diff --git a/components/feedback/feedback_util_unittest.cc b/components/feedback/feedback_util_unittest.cc
index 95f30b2..5e066524 100644
--- a/components/feedback/feedback_util_unittest.cc
+++ b/components/feedback/feedback_util_unittest.cc
@@ -153,4 +153,43 @@
   EXPECT_EQ(autofill_data_str, expected_autofill_data_str);
 }
 
+TEST_F(FeedbackUtilTest, ZipStringTraversal) {
+  // Create a temp directory, and target a file within it:
+  base::ScopedTempDir root_dir;
+  ASSERT_TRUE(root_dir.CreateUniqueTempDir());
+  base::FilePath sensitive_file =
+      root_dir.GetPath().AppendASCII("sensitive.txt");
+
+  // Construct a traversal back to that file in a platform-dependent way:
+  std::string sensitive_path_str = sensitive_file.AsUTF8Unsafe();
+#if BUILDFLAG(IS_WIN)
+  // Remove "C:" if present
+  if (sensitive_path_str.size() >= 2 && sensitive_path_str[1] == ':') {
+    sensitive_path_str = sensitive_path_str.substr(2);
+  }
+  // Remove leading backslash
+  if (!sensitive_path_str.empty() && sensitive_path_str[0] == '\\') {
+    sensitive_path_str = sensitive_path_str.substr(1);
+  }
+  base::FilePath traversal(
+      FILE_PATH_LITERAL("..\\..\\..\\..\\..\\..\\..\\..\\"));
+#else
+  // Remove leading slash
+  if (!sensitive_path_str.empty() && sensitive_path_str[0] == '/') {
+    sensitive_path_str = sensitive_path_str.substr(1);
+  }
+  base::FilePath traversal(FILE_PATH_LITERAL("../../../../../../../../"));
+#endif
+
+  base::FilePath malicious_filename =
+      traversal.Append(base::FilePath::FromUTF8Unsafe(sensitive_path_str));
+
+  // Call ZipString.
+  std::optional<std::string> result =
+      feedback_util::ZipString(malicious_filename, "maliciousness");
+
+  EXPECT_FALSE(result.has_value());
+  EXPECT_FALSE(base::PathExists(sensitive_file));
+}
+
 }  // namespace feedback_util
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/feedback/feedback_util_unittest.cc b/components/feedback/feedback_util_unittest.cc
index 95f30b2..5e066524 100644
--- a/components/feedback/feedback_util_unittest.cc
+++ b/components/feedback/feedback_util_unittest.cc
@@ -153,4 +153,43 @@
   EXPECT_EQ(autofill_data_str, expected_autofill_data_str);
 }
 
+TEST_F(FeedbackUtilTest, ZipStringTraversal) {
+  // Create a temp directory, and target a file within it:
+  base::ScopedTempDir root_dir;
+  ASSERT_TRUE(root_dir.CreateUniqueTempDir());
+  base::FilePath sensitive_file =
+      root_dir.GetPath().AppendASCII("sensitive.txt");
+
+  // Construct a traversal back to that file in a platform-dependent way:
+  std::string sensitive_path_str = sensitive_file.AsUTF8Unsafe();
+#if BUILDFLAG(IS_WIN)
+  // Remove "C:" if present
+  if (sensitive_path_str.size() >= 2 && sensitive_path_str[1] == ':') {
+    sensitive_path_str = sensitive_path_str.substr(2);
+  }
+  // Remove leading backslash
+  if (!sensitive_path_str.empty() && sensitive_path_str[0] == '\\') {
+    sensitive_path_str = sensitive_path_str.substr(1);
+  }
+  base::FilePath traversal(
+      FILE_PATH_LITERAL("..\\..\\..\\..\\..\\..\\..\\..\\"));
+#else
+  // Remove leading slash
+  if (!sensitive_path_str.empty() && sensitive_path_str[0] == '/') {
+    sensitive_path_str = sensitive_path_str.substr(1);
+  }
+  base::FilePath traversal(FILE_PATH_LITERAL("../../../../../../../../"));
+#endif
+
+  base::FilePath malicious_filename =
+      traversal.Append(base::FilePath::FromUTF8Unsafe(sensitive_path_str));
+
+  // Call ZipString.
+  std::optional<std::string> result =
+      feedback_util::ZipString(malicious_filename, "maliciousness");
+
+  EXPECT_FALSE(result.has_value());
+  EXPECT_FALSE(base::PathExists(sensitive_file));
+}
+
 }  // namespace feedback_util
Loading diff…

Original Bug Report

reported by [email protected]

Arbitrary file write via path traversal in feedback_util::ZipString

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: A path traversal vulnerability exists in the chrome.feedbackPrivate.sendFeedback API that could allow a compromised WebUI renderer to perform an arbitrary file write. The filename provided in the attachedFile object is inadequately sanitized and appended to a temporary directory path before being written to disk uncompressed. This allows an attacker to escape the temporary directory and overwrite sensitive files in the context of the unsandboxed Browser process.

Affected files:

  • components/feedback/feedback_util.cc
  • extensions/browser/api/feedback_private/feedback_private_api.cc
  • components/feedback/feedback_common.cc
  • extensions/browser/api/feedback_private/feedback_service.cc
  • components/feedback/feedback_data.cc

Estimated timestamp from git blame: 2024-08-22

Summary

A potential path traversal vulnerability exists in the implementation of the chrome.feedbackPrivate.sendFeedback extension API. The vulnerability could allow a compromised renderer process—specifically one hosting an authorized WebUI (like chrome://feedback)—to perform arbitrary file writes in the context of the browser process. This can lead to a sandbox escape and persistent code execution by overwriting sensitive files such as ~/.bashrc on Linux or adding files to the Startup folder on Windows.

Vulnerability Details

When a renderer calls chrome.feedbackPrivate.sendFeedback, it can optionally provide an AttachedFile object containing a name (the filename) and a blob UUID representing the file’s data. The handling of this filename allows for a path traversal attack:

  1. Entry Point & Weak Sanitization: In FeedbackPrivateSendFeedbackFunction::Run (extensions/browser/api/feedback_private/feedback_private_api.cc), the provided name is passed to StripFakepath. This function only removes a literal C:\fakepath\ prefix and does not perform any validation for path traversal sequences like .. or ..\.
  2. Data Flow: The unsanitized string is stored in FeedbackData. Later, FeedbackData::AttachAndCompressFileData converts this string into a base::FilePath using FilePath::FromUTF8Unsafe, which performs no path normalization or traversal checks.
  3. Vulnerable Sink: The data flows to FeedbackCommon::CompressFile and then to feedback_util::ZipString(filename, data) (components/feedback/feedback_util.cc). Inside ZipString, a temporary directory is created.
  4. Path Traversal: The code attempts to write the uncompressed blob data to a file inside the temporary directory using:
    base::WriteFile(temp_dir.GetPath().Append(filename), data)
    
    base::FilePath::Append performs raw string concatenation and does not prevent the use of .. relative path components. If filename is set to ../../../../../../home/user/.bashrc, the resulting path evaluates to that exact string.
  5. Arbitrary Write: base::WriteFile passes this traversed path directly to underlying OS APIs (like creat or CreateFile). The operating system resolves the .. sequences, breaking out of the temporary directory, and writes the attacker’s raw, uncompressed payload data to the resolved path. Because the Browser process runs unsandboxed with user privileges, the write succeeds.
  6. Persistence: The base::ScopedTempDir destructor only deletes the temporary directory it created. The maliciously written file resides outside this directory and persists.

Impact

An attacker who has compromised a WebUI renderer (e.g., via a V8 bug or XSS in chrome://feedback) can escalate their privileges to the browser process context. By writing to sensitive filesystem locations, the attacker can achieve persistent, unsandboxed code execution.

Suggested Reproduction Steps

Note: These are suggested steps based on code analysis; our tooling agent cannot execute code to verify them.

  1. Compromise a renderer process hosting an authorized WebUI (e.g., chrome://feedback).
  2. Construct a JavaScript Blob containing the malicious payload (e.g., a reverse shell script or malware executable) and retrieve its UUID.
  3. Invoke the chrome.feedbackPrivate.sendFeedback API with an attachedFile object.
  4. Set the attachedFile.name to a traversal string targeting a sensitive file (e.g., ../../../../../../home/user/.bashrc).
  5. The browser process will read the blob and write its contents to the specified path outside the intended temporary directory.

Suggested Fix

To prevent path traversal, the filename should be strictly validated before being appended to the temporary directory path.

  1. In feedback_util::ZipString (or earlier in the pipeline when the FilePath is created), verify that the path does not contain traversal components by checking filename.ReferencesParent().
  2. Alternatively, use base::SafeBaseName to ensure the provided filename only contains a single, safe path component without any directory separators or parent references.

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 1033360

View on issue tracker