Fix presubmit and postsubmit path handling vulnerabilities (#2917) Addresses b/569811973. - `bcr_presubmit.py`: Uses `git diff -z` to parse quoted paths correctly, preventing a bypass of symlink validations. - `bcr_postsubmit.py`: Verifies that the resolved path of the destination attestation file is contained within the expected output directory using `pathlib.Path.resolve()`, preventing arbitrary file writes via malicious symlinks.
diff --git a/buildkite/bazel-central-registry/bcr_postsubmit.py b/buildkite/bazel-central-registry/bcr_postsubmit.py index 78425b0..489d7ac 100755 --- a/buildkite/bazel-central-registry/bcr_postsubmit.py +++ b/buildkite/bazel-central-registry/bcr_postsubmit.py
@@ -27,6 +27,7 @@ import hashlib import json import os +import pathlib import requests import subprocess import sys @@ -123,6 +124,14 @@ print("\t\tIntegrity: OK") dest = os.path.join(dest_dir, get_canonical_basename(url)) + + dest_path = pathlib.Path(dest).resolve() + dest_dir_path = pathlib.Path(dest_dir).resolve() + if not dest_path.is_relative_to(dest_dir_path): + raise AttestationError( + f"Resolved path {dest_path} is outside of expected directory {dest_dir_path}" + ) + print(f"\t\tWriting attestation to {dest}...") with open(dest, "wb") as f: f.write(raw_content)
diff --git a/buildkite/bazel-central-registry/bcr_presubmit.py b/buildkite/bazel-central-registry/bcr_presubmit.py index 4c4ed78..cacd22a 100755 --- a/buildkite/bazel-central-registry/bcr_presubmit.py +++ b/buildkite/bazel-central-registry/bcr_presubmit.py
@@ -96,11 +96,15 @@ """ # Get the list of changed files compared to the main branch output = subprocess.check_output( - ["git", "diff", "main...HEAD", "--name-only", "--pretty=format:"] + ["git", "diff", "main...HEAD", "--name-only", "-z", "--pretty=format:"] ) modules = set() # Matching modules/<name>/<version>/ - for line in output.decode("utf-8").split(): + # `git diff -z --name-only` delimits affected files with \0 and outputs paths verbatim + # (without quoting unusual characters), so split on \0 instead of whitespace. + for line in output.decode("utf-8").split("\0"): + if not line: + continue s = re.match(r"modules\/([^\/]+)\/([^\/]+)\/", line) if s: module_name, module_version = s.groups() @@ -127,11 +131,13 @@ """ # Get the list of changed files compared to the main branch output = subprocess.check_output( - ["git", "diff", "main...HEAD", "--name-only", "--pretty=format:"] + ["git", "diff", "main...HEAD", "--name-only", "-z", "--pretty=format:"] ) modules = set() # Matching modules/<name>/metadata.json - for line in output.decode("utf-8").split(): + for line in output.decode("utf-8").split("\0"): + if not line: + continue s = re.match(r"modules\/([^\/]+)\/metadata\.json", line) if s: module_name = s.groups()[0] @@ -365,7 +371,7 @@ # Get all files that are Modified, Renamed, or Deleted. bazelci.print_expanded_group("Checking if existing modules are not modified") output = subprocess.check_output( - ["git", "diff", "main...HEAD", "--diff-filter=MRD", "--name-only", "--pretty=format:"] + ["git", "diff", "main...HEAD", "--diff-filter=MRD", "--name-only", "-z", "--pretty=format:"] ) # Check if any of the source.json, MODULE.bazel, or patch files are changed for an existing module. @@ -376,7 +382,9 @@ re.compile(r"modules\/([^\/]+)\/([^\/]+)\/overlay"), ] changed_modules = [] - for line in output.decode("utf-8").split(): + for line in output.decode("utf-8").split("\0"): + if not line: + continue for p in NO_CHANGE_FILE_PATTERNS: s = p.match(line) if s: @@ -394,10 +402,14 @@ return bazelci.print_expanded_group("Checking if any file changes outside of modules/") output = subprocess.check_output( - ["git", "diff", "main...HEAD", "--name-only", "--pretty=format:", ":!modules"] - ).decode("utf-8").strip() - if output: - error("The following files should not be changed when adding a new module version:\n" + output) + ["git", "diff", "main...HEAD", "--name-only", "-z", "--pretty=format:", ":!modules"] + ).decode("utf-8") + changed_files = [f for f in output.split("\0") if f] + if changed_files: + error( + "The following files should not be changed when adding a new module version:\n" + + "\n".join(changed_files) + ) else: bazelci.eprint("Nothing changed outside of modules/")