fix(ci): reject a checking_base_sha that is not a git object name - #1785
Conversation
`ci git-refs` took the merge-queue `checking_base_sha` verbatim out of the merge-queue draft pull request body and emitted it as `base`. `queue_metadata::extract_from_event` admits a body on the title prefix `merge queue: ` and nothing else, so whoever opens the pull request writes that value, fork PRs included. `base` then leaves the CLI through several sinks that read a leading `-` as an option: `$GITHUB_OUTPUT` and the `--format=shell` eval hand it to the caller's workflow, `ci scopes` puts it in git invocations, and both commands echo it to stderr, which GitHub Actions also scans for `::` workflow commands. The reporter's proof run put `checking_base_sha: --output=/home/runner/.gitconfig` in a fork PR body and truncated that file inside the job. Require a full hex object name, 40 digits or 64, before the value can become a base. Abbreviations are out on purpose: `scopes_detect::changed_files::is_sha` routes only full SHAs as revisions, so a short one is fetched as a branch name and fails the run. Nothing honest is lost, the engine types the field `github_types.SHAType` and writes a full SHA. The gate is on the pull request body alone, not on the git note that carries the same field. That asymmetry is deliberate: the note comes from the engine over a push to `origin`, and a note the check rejected would fall through to the body path, handing the untrusted payload the precedence the note is meant to hold, silently, since `real_notes_reader` has no `Output` to warn through. A rejected value is not metadata, so it falls through to the pull request's own base with a warning, the shape already used when the key is missing. The warning renders the value escaped and cut to a bounded length, so it can neither start a workflow command of its own nor fill the log. Reported as HackerOne #3965784, closed Informative: the blast radius is the ephemeral runner the pull request itself triggered. Fixes MRGFY-8845 Change-Id: Ie24ad032ac9c96b1231e76bc30ece50cd905d8d2
|
This pull request is part of a Mergify stack:
|
Merge Protections🟢 All 6 merge protections satisfied — ready to merge. Show 6 satisfied protections🟢 🤖 Continuous Integration
🟢 👀 Review Requirements
🟢 Enforce conventional commitMake sure that we follow https://www.conventionalcommits.org/en/v1.0.0/
🟢 🔎 Reviews
🟢 📕 PR description
🟢 🚦 Auto-queueWhen all merge protections are satisfied, this pull request will be queued automatically. |
There was a problem hiding this comment.
Pull request overview
This PR hardens mergify ci git-refs against injection via untrusted merge-queue draft PR bodies by validating checking_base_sha before treating it as a git revision, preventing option/command-style payloads from propagating into downstream sinks (e.g., $GITHUB_OUTPUT, shell eval, git invocations, and GitHub Actions workflow-command parsing).
Changes:
- Validate merge-queue PR-body
checking_base_shawith a strict “full hex object name” check (40 or 64 lowercase hex chars) before using it asbase. - Add bounded, escaped warning rendering for rejected values and fall back to the PR base with an explicit warning.
- Add targeted unit tests covering acceptance/rejection, warning safety, and fallback behavior.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Merge Queue Status
This pull request spent 56 seconds in the queue, including 12 seconds running CI. Required conditions to merge
|
`ci queue-info` and `ci scopes` each carried their own copy of the "open $GITHUB_OUTPUT, draw a random ghadelimiter_ suffix, write the heredoc" sequence, including a byte-for-byte duplicate of `random_delimiter_suffix`. `ci git-refs` needs the same thing next, which would have made three. Move it to a `github_output` module that takes the `(name, value)` pairs and does the rest, so a call site cannot pick the bare `name=value` form by accident: the heredoc is the only form the module emits, and its delimiter comes fresh from the OS RNG per output. The name half of the pair is `&'static str` rather than `&str`. A newline in a name, or an `=` ahead of the `<<`, lets the runner read the block as something else, and the type keeps a name derived from a payload out by construction. The block is assembled and written once. Three `writeln!` calls on an unbuffered `File` are several `write_all` syscalls each, and a sequence cut in the middle leaves an unterminated heredoc, which fails the step outright rather than losing a line. `junit_process` keeps its own bare-form writer; the module says why. What is written does not change. GitHub parses both forms into the same output value, and both migrated call sites already wrote the heredoc. Each now builds its payload before the `$GITHUB_OUTPUT` check rather than after, so off a GitHub Actions runner one small string is built and dropped. Fixes MRGFY-8845 Depends-On: #1785
ci git-refstook the merge-queuechecking_base_shaverbatim out ofthe merge-queue draft pull request body and emitted it as
base.queue_metadata::extract_from_eventadmits a body on the title prefixmerge queue:and nothing else, so whoever opens the pull requestwrites that value, fork PRs included.
basethen leaves the CLI through several sinks that read a leading-as an option:$GITHUB_OUTPUTand the--format=shelleval handit to the caller's workflow,
ci scopesputs it in git invocations,and both commands echo it to stderr, which GitHub Actions also scans
for
::workflow commands. The reporter's proof run putchecking_base_sha: --output=/home/runner/.gitconfigin a fork PR bodyand truncated that file inside the job.
Require a full hex object name, 40 digits or 64, before the value can
become a base. Abbreviations are out on purpose:
scopes_detect::changed_files::is_sharoutes only full SHAs asrevisions, so a short one is fetched as a branch name and fails the
run. Nothing honest is lost, the engine types the field
github_types.SHATypeand writes a full SHA.The gate is on the pull request body alone, not on the git note that
carries the same field. That asymmetry is deliberate: the note comes
from the engine over a push to
origin, and a note the check rejectedwould fall through to the body path, handing the untrusted payload the
precedence the note is meant to hold, silently, since
real_notes_readerhas noOutputto warn through.A rejected value is not metadata, so it falls through to the pull
request's own base with a warning, the shape already used when the key
is missing. The warning renders the value escaped and cut to a bounded
length, so it can neither start a workflow command of its own nor fill
the log.
Reported as HackerOne #3965784, closed Informative: the blast radius is
the ephemeral runner the pull request itself triggered.
Fixes MRGFY-8845