Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in UI
DescriptionInsufficient validation of untrusted input in UI
ComponentUI
Bug ClassLogic Error
Tracker518105731
Fix commit633de441a4c8 (chromium/src) +44/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-08

Changed Functions

FunctionChangeNotes
TEST
ui/base/x/x11_cursor_loader_unittest.cc
modified
for
ui/base/x/x11_cursor_loader_unittest.cc
modified

Files Changed

  • ui/base/x/x11_cursor_loader.cc
  • ui/base/x/x11_cursor_loader.h
  • ui/base/x/x11_cursor_loader_unittest.cc
From 633de441a4c8d326e1575eef15167d9e38e4cdb1 Mon Sep 17 00:00:00 2001
From: Tom Anderson <thomasanderson@chromium.org>
Date: Mon, 01 Jun 2026 14:50:58 -0700
Subject: [PATCH] Add validation to cursor theme name to prevent path traversal.

On Linux Ozone/X11 platforms, a compromised GPU process could
potentially exploit its cloned X11 connection to hijack the XSETTINGS
selection, supplying a malicious cursor theme name.

This CL remediates the issue by sanitizing and validating any cursor
theme name retrieved from GetCursorThemeName() or recursive inherits
parsing during the cursor loading process, rejecting absolute paths,
relative parent references, and paths with multiple components.

Fixed: 518105731
Change-Id: Ia0f9eb7b433dc1d7a200485dfb70e1ec67e68910
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7890525
Reviewed-by: Lei Zhang <thestig@chromium.org>
Commit-Queue: Thomas Anderson <thomasanderson@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1639694}
---

diff --git a/ui/base/x/x11_cursor_loader.cc b/ui/base/x/x11_cursor_loader.cc
index 7188541..ad5feb08 100644
--- a/ui/base/x/x11_cursor_loader.cc
+++ b/ui/base/x/x11_cursor_loader.cc
@@ -152,12 +152,22 @@
   return path;
 }
 
+bool IsValidCursorThemeName(const std::string& theme) {
+  base::FilePath theme_path(theme);
+  return !theme.empty() && theme != "." && !theme_path.IsAbsolute() &&
+         !theme_path.ReferencesParent() && theme_path.BaseName() == theme_path;
+}
+
 scoped_refptr<base::RefCountedMemory> ReadCursorFromThemeImpl(
     const std::string& theme,
     const std::string& cursor_name,
     base::flat_set<ThemeAndCursorName>* parent_theme_and_cursor_names,
     base::flat_map<ThemeAndCursorName, scoped_refptr<base::RefCountedMemory>>*
         cache) {
+  if (!IsValidCursorThemeName(theme)) {
+    return nullptr;
+  }
+
   constexpr const char kCursorDir[] = "cursors";
   constexpr const char kThemeInfo[] = "index.theme";
 
@@ -267,6 +277,10 @@
 
 }  // namespace
 
+bool IsValidCursorThemeNameForTesting(const std::string& theme) {
+  return IsValidCursorThemeName(theme);
+}
+
 XCursorLoader::XCursorLoader(x11::Connection* connection,
                              base::RepeatingClosure on_cursor_config_changed)
     : connection_(connection),
diff --git a/ui/base/x/x11_cursor_loader.h b/ui/base/x/x11_cursor_loader.h
index 94d0872..570a70ce 100644
--- a/ui/base/x/x11_cursor_loader.h
+++ b/ui/base/x/x11_cursor_loader.h
@@ -90,6 +90,9 @@
 };
 
 COMPONENT_EXPORT(UI_BASE_X)
+bool IsValidCursorThemeNameForTesting(const std::string& theme);
+
+COMPONENT_EXPORT(UI_BASE_X)
 std::vector<XCursorLoader::Image> ParseCursorFile(
     scoped_refptr<base::RefCountedMemory> file,
     uint32_t preferred_size);
diff --git a/ui/base/x/x11_cursor_loader_unittest.cc b/ui/base/x/x11_cursor_loader_unittest.cc
index d72f51d..3cdf6db 100644
--- a/ui/base/x/x11_cursor_loader_unittest.cc
+++ b/ui/base/x/x11_cursor_loader_unittest.cc
@@ -262,4 +262,31 @@
   EXPECT_EQ(images[1].frame_delay.InMilliseconds(), 500);
 }
 
+TEST(XCursorLoaderTest, ThemeNameValidation) {
+  const char* const kInvalidThemes[] = {
+      "",
+      ".",
+      "..",
+      "/foo",
+      "/tmp/evil",
+      "../foo",
+      "foo/..",
+      "../../../../tmp/poc-cursor-evil",
+      "foo/bar",
+  };
+  for (const char* theme : kInvalidThemes) {
+    EXPECT_FALSE(IsValidCursorThemeNameForTesting(theme));
+  }
+
+  const char* const kValidThemes[] = {
+      "default",
+      "Adwaita",
+      "DMZ-White",
+      "my_theme-123",
+  };
+  for (const char* theme : kValidThemes) {
+    EXPECT_TRUE(IsValidCursorThemeNameForTesting(theme));
+  }
+}
+
 }  // namespace ui
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ui/base/x/x11_cursor_loader_unittest.cc b/ui/base/x/x11_cursor_loader_unittest.cc
index d72f51d..3cdf6db 100644
--- a/ui/base/x/x11_cursor_loader_unittest.cc
+++ b/ui/base/x/x11_cursor_loader_unittest.cc
@@ -262,4 +262,31 @@
   EXPECT_EQ(images[1].frame_delay.InMilliseconds(), 500);
 }
 
+TEST(XCursorLoaderTest, ThemeNameValidation) {
+  const char* const kInvalidThemes[] = {
+      "",
+      ".",
+      "..",
+      "/foo",
+      "/tmp/evil",
+      "../foo",
+      "foo/..",
+      "../../../../tmp/poc-cursor-evil",
+      "foo/bar",
+  };
+  for (const char* theme : kInvalidThemes) {
+    EXPECT_FALSE(IsValidCursorThemeNameForTesting(theme));
+  }
+
+  const char* const kValidThemes[] = {
+      "default",
+      "Adwaita",
+      "DMZ-White",
+      "my_theme-123",
+  };
+  for (const char* theme : kValidThemes) {
+    EXPECT_TRUE(IsValidCursorThemeNameForTesting(theme));
+  }
+}
+
 }  // namespace ui
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential GPU-to-Browser Sandbox Escape via GTK XSETTINGS and Cursor Theme Path Traversal

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A compromised GPU process can potentially exploit its cloned X11 connection to hijack the XSETTINGS selection and supply a malicious cursor theme name. Because the browser process does not sanitize this retrieved cursor theme name, it can lead to a directory traversal in the cursor loader. This allows the browser to traverse directories and parse arbitrary cursor files outside of the intended directories.

Affected files:

  • ui/base/x/x11_cursor_loader.cc
  • ui/gtk/gtk_ui.cc

Estimated timestamp from git blame: 2020-07-17

Description and Root Cause

On Linux Ozone/X11 platforms, the sandboxed GPU process retains a cloned, live X11 connection before sandbox lockdown to facilitate initialization. Specifically, in ui/ozone/platform/x11/ozone_platform_x11.cc:

// Set up the X11 connection before the sandbox gets set up.
auto connection = x11::Connection::Get()->Clone();
connection->DetachFromSequence();
surface_factory_ozone_ = std::make_unique<X11SurfaceFactory>(std::move(connection));

Meanwhile, the browser process runs the GDK/GTK backend in-process, which monitors XSETTINGS properties (specifically the _XSETTINGS_SETTINGS property on the selection owner of _XSETTINGS_S0) to propagate system-wide settings. GtkUi registers for notifications on the gtk-cursor-theme-name setting (ui/gtk/gtk_ui.cc):

connect(settings, "notify::gtk-cursor-theme-name", &GtkUi::OnCursorThemeNameChanged);

When a change is signaled, GtkUi::GetCursorThemeName retrieves the property string verbatim and propagates it without sanitization:

std::string GtkUi::GetCursorThemeName() {
  gchar* theme = nullptr;
  g_object_get(gtk_settings_get_default(), "gtk-cursor-theme-name", &theme, nullptr);
  ...
  return theme_string;
}

Downstream in ui/base/x/x11_cursor_loader.cc, XCursorLoader reads and loads the cursor files on a background ThreadPool thread. In ReadCursorFromThemeImpl, it traverses search directories and appends the theme name directly using base::FilePath::Append:

for (const auto& path : paths) {
  auto dir = CanonicalizePath(base::FilePath(path));
  if (dir.empty())
    continue;
  base::FilePath theme_dir = dir.Append(theme); // Path traversal vulnerability
  base::FilePath cursor_dir = theme_dir.Append(kCursorDir);

  std::string contents;
  if (base::ReadFileToString(cursor_dir.Append(cursor_name), &contents)) {
    ...
  }
}

Because base::FilePath::Append does not validate path components in release builds (it only contains a DCHECK(!IsPathAbsolute(appended)) which is stripped on release and does not block .. parent directory segments), a theme name containing .. sequences can successfully escape the standard icon directories. The browser process will then attempt to read and parse files at the resolved path via ParseCursorFile.

Potential Exploitation Scenario

Note: These are suggested/potential steps; we do not currently have a fully running proof of concept.

  1. An attacker gains code execution in the sandboxed GPU process.
  2. The attacker uses the cloned X11 connection to create an X11 window.
  3. The attacker writes a malicious XSETTINGS payload containing a path traversal string (e.g., ../../../../tmp/evil_theme) to the _XSETTINGS_SETTINGS property on the window.
  4. The attacker claims ownership of the _XSETTINGS_S0 selection and broadcasts a MANAGER ClientMessage to notify the root window.
  5. GDK in the browser process intercepts this, parses the payload in-process, and updates GtkSettings.
  6. The notify::gtk-cursor-theme-name signal fires, clearing the cached cursors in X11CursorFactory::ClearThemeCursors.
  7. Upon the next cursor state update, the browser process requests a cursor reload, invoking ReadCursorFromThemeImpl which appends the traversal path, resolves the absolute target location, reads the file, and parses it inside the browser process.

Suggested Remediation

To remediate this issue, the browser process should validate and sanitize any cursor theme name retrieved from GetCursorThemeName() or during the cursor loading process.

Specifically, you can reject theme names that contain references to parent directories or absolute paths, or enforce that the theme string consists of only alphanumeric characters, dashes, and underscores:

// Verify that the theme name does not attempt directory traversal
base::FilePath theme_path(theme);
if (theme_path.ReferencesParent() || theme_path.IsAbsolute()) {
  return nullptr; // or fallback to "default"
}

Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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.

View on issue tracker