Skip to content

fix(ci): reject a checking_base_sha that is not a git object name - #1785

Merged
mergify[bot] merged 1 commit into
mainfrom
devs/sileht/mrgfy-8845-git-refs-base-validation/reject-checking-base-sha-git-object-name--e24ad032
Aug 25, 2026
Merged

fix(ci): reject a checking_base_sha that is not a git object name#1785
mergify[bot] merged 1 commit into
mainfrom
devs/sileht/mrgfy-8845-git-refs-base-validation/reject-checking-base-sha-git-object-name--e24ad032

Conversation

@sileht

@sileht sileht commented Aug 25, 2026

Copy link
Copy Markdown
Member

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

`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
Copilot AI lite review requested due to automatic review settings August 25, 2026 10:42
@sileht

sileht commented Aug 25, 2026

Copy link
Copy Markdown
Member Author

This pull request is part of a Mergify stack:

# Pull Request Link
1 fix(ci): reject a checking_base_sha that is not a git object name #1785 👈
2 refactor(ci): share one $GITHUB_OUTPUT heredoc writer #1786
3 fix(ci): write git-refs base/head through the heredoc writer #1787

@mergify
mergify Bot deployed to Mergify Merge Protections August 25, 2026 10:42 Active
@sileht
sileht deployed to func-tests-live August 25, 2026 10:42 — with GitHub Actions Active
@mergify

mergify Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 All 6 merge protections satisfied — ready to merge.

Show 6 satisfied protections

🟢 🤖 Continuous Integration

  • all of:
    • check-success=ci-gate

🟢 👀 Review Requirements

  • any of:
    • #approved-reviews-by>=2
    • author = dependabot[bot]
    • author = mergify-ci-bot
    • author = renovate[bot]

🟢 Enforce conventional commit

Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/

  • title ~= ^(fix|feat|internal|docs|style|refactor|perf|test|build|ci|chore|revert|ui)(?:\(.+\))?!?:

🟢 🔎 Reviews

  • #changes-requested-reviews-by = 0
  • #review-requested = 0
  • #review-threads-unresolved = 0

🟢 📕 PR description

  • body ~= (?ms:.{48,})

🟢 🚦 Auto-queue

When all merge protections are satisfied, this pull request will be queued automatically.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_sha with a strict “full hex object name” check (40 or 64 lowercase hex chars) before using it as base.
  • 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.

@mergify
mergify Bot requested a review from a team August 25, 2026 10:48
@sileht
sileht marked this pull request as ready for review August 25, 2026 11:46
@mergify
mergify Bot requested a review from a team August 25, 2026 12:48
@mergify

mergify Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Merge Queue Status

  • Entered queue2026-08-25 14:25 UTC · Rule: default · triggered by merge protections
  • Checks skipped · PR is already up-to-date
  • Merged2026-08-25 14:26 UTC · at 1405948c2d04b869ccc10073b8056c7d03ad6dc0 · squash

This pull request spent 56 seconds in the queue, including 12 seconds running CI.

Required conditions to merge

@mergify mergify Bot added the queued label Aug 25, 2026
@mergify
mergify Bot merged commit caba8c3 into main Aug 25, 2026
23 checks passed
@mergify
mergify Bot deleted the devs/sileht/mrgfy-8845-git-refs-base-validation/reject-checking-base-sha-git-object-name--e24ad032 branch August 25, 2026 14:26
@mergify mergify Bot removed the queued label Aug 25, 2026
mergify Bot pushed a commit that referenced this pull request Aug 25, 2026
`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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants