High watch CVSS 8.3 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Feedback
DescriptionInsufficient validation of untrusted input in Feedback
ComponentFeedback
Bug ClassInput Validation
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker502248774
Fix commit30d636a452df (chromium/src) +47/-1
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-13 (per vendor advisory)
Fix landed2026-04-14 (public git)
Disclosed2026-04-28
Patch gap14 days — fix public in source before the advisory

Background

`feedback_util::ZipString`
a helper that writes a caller-supplied data string into a file named filename inside a freshly created temporary directory and then zips it.
Path traversal
supplying a filename containing ../ (or ..\) components so that Append-ing it to a base directory resolves to a location outside that directory.
`base::FilePath::Append`
joins a relative path onto a base path; it does not sanitize .. components, so a traversal payload escapes the base.
`base::SafeBaseName`
a wrapper constructed via SafeBaseName::Create that holds only a single, final path component (a base name), rejecting or stripping any directory parts.

Root Cause Analysis

ZipString took a base::FilePath filename that could originate from an untrusted source and used it directly in base::WriteFile(temp_dir.GetPath().Append(filename), data). Because Append does not neutralize .. segments, a filename such as ../../../.../target resolves outside the temp directory, so the attacker-chosen data is written to an attacker-chosen path — a path-traversal/arbitrary-write.

The patch adds a guard at function entry: it builds base::SafeBaseName::Create(filename) and returns std::nullopt unless the reconstructed base name equals the original (safe_name->path() != filename), i.e. the filename must already be a single safe component with no directory parts. It then writes using safe_name->path() rather than the raw filename. This tightens the contract: any multi-component or traversal filename now fails closed before any file is written, as the shipped ZipStringTraversal test confirms by asserting the sensitive target file is never created.

Key insight
The core mistake was Append-ing an untrusted filename (which may contain ..) onto the temp directory with no base-name validation; the fix requires the filename to be an unchanged single-component SafeBaseName and bails out otherwise.

Attack Path

  1. Reach `ZipString` with a controlled filename A compromised renderer drives the feedback flow so that ZipString is invoked with an attacker-chosen filename and data (per the commit message’s stated threat model).
  2. Embed traversal segments The filename is crafted with many leading ../ (or ..\ on Windows) components followed by an absolute-ish target path, as the regression test constructs.
  3. Escape the temp directory temp_dir.GetPath().Append(filename) resolves outside the unique temp dir to an attacker-selected location.
  4. Write attacker data out of bounds of the directory base::WriteFile creates/overwrites that target file with the supplied data, yielding an out-of-directory file write.
  5. Downstream abuse Converting an arbitrary file write into persistence or code execution is (inferred); the patch and test demonstrate only the escape and the write.

Impact Assessment

An attacker able to control the filename reaching ZipString (the commit cites a compromised renderer) can write chosen contents to a path outside the intended temporary directory, since feedback_util runs in the browser process with its privileges. The advisory rates this high and does not report in-the-wild exploitation. The precondition is renderer compromise (or another untrusted caller of ZipString); escalation of the arbitrary write to code execution is (inferred).

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

Audit Directions

  • Unvalidated `Append` of untrusted paths
    Grep for FilePath::Append/AppendASCII where the appended component derives from IPC, renderer, or network input without a SafeBaseName or explicit ../absolute-path check.
  • Base-name assumptions in file writers
    Review helpers that treat a passed FilePath as “just a filename” before WriteFile/CreateDirectory, especially in the feedback, downloads, and crash-report paths that accept renderer-supplied names.
  • Equality-check validation idiom
    Look for places that should adopt the SafeBaseName::Create(x)->path() == x pattern (reject if normalization changes the value) but instead sanitize in place, which can silently alter rather than reject malicious input.
From 30d636a452dfa0076ffdfe2826c358b3744bf902 Mon Sep 17 00:00:00 2001
From: Mike West <mkwst@chromium.org>
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 <xiangdongkong@google.com>
Commit-Queue: Mike West <mkwst@chromium.org>
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 vm...@google.com

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