Skip to content

Harden ob1-gate.yml against gate-bypass holes#2

Merged
Humestone merged 1 commit into
mainfrom
fix/ob1-gate-hardening
Jun 10, 2026
Merged

Harden ob1-gate.yml against gate-bypass holes#2
Humestone merged 1 commit into
mainfrom
fix/ob1-gate-hardening

Conversation

@Humestone

Copy link
Copy Markdown
Owner

Summary

PR #1 added a ready_for_review trigger to the OB1 PR gate and assessed that "the gate already blocks." An adversarial review found pre-existing holes in ob1-gate.yml that defeat that blocking guarantee. This PR fixes them. No other file is touched.

Fixes

# Sev Attack / failure mode Old behavior New behavior
1 HIGH Shell injection via PR title. PR_TITLE="${{ github.event.pull_request.title }}" (and the same raw interpolation of title/number/login/sha in the artifact step) was substituted into the run: script body before the shell parsed it. A crafted title (e.g. containing $(...), backticks, or "; echo "failed=false" >> $GITHUB_OUTPUT; #) executed arbitrary shell in the runner and could force the gate green. All untrusted, PR-author-controlled values (title, changed-file list, author login, etc.) are passed via env: and referenced as quoted shell variables. The title is now inert data; injection payloads do not execute.
2 HIGH [docs] title bypass. The skip decision keyed off the PR title prefix ^\[docs\]. Any PR titled [docs] ... hit exit 0 before any check ran, including the credential scan and binary-blob check — regardless of which files were actually changed. A secret could be merged under a [docs] title. Skip is decided only by the actual changed-file list (skip contribution checks when no contribution folders are touched). Even in that path, the credential scan and binary-blob scan always run and still set failed=true to block. The title is no longer authoritative for skipping anything.
3 MEDIUM Shallow base fetch under-reports changes. Base was fetched --depth=1; the diff used origin/main...HEAD with a silent fallback to HEAD~1 HEAD. When main had advanced, the merge base could be lost and the fallback diffed only the last commit of a multi-commit PR, so checks under-reported the changed files. fetch-depth: 0 on checkout, full base fetch (no --depth), merge base verified with git merge-base, and the diff has no last-commit-only fallback — it fails closed if the diff cannot be computed.
4 LOW Rule 6 (category artifacts) counting bug. `readme_has_steps=$(grep -c ... echo "0")`.

Note on check count

This PR does not change it, but for the record: Rule 11 (LLM clarity review) is a hardcoded pass stub — it calls pass_check unconditionally and defers the actual review to a separate claude-review.yml workflow. So the advertised "15 checks" are effectively 14 enforceable within this gate.

Verification

  • actionlint (installed via Homebrew): the modified workflow introduces no new finding classes vs. the pre-change baseline. The only delta my edits added (two SC2086 quote-$GITHUB_OUTPUT infos) were fixed by quoting "$GITHUB_OUTPUT" throughout. Remaining findings (SC2001, SC2034, SC2129 style/info) are all pre-existing and untouched by this PR.
  • bash -n on every extracted run: script: all pass (exit 0).
  • Behavioral simulation of the review script against a sandbox repo:
    • [docs]-titled PR containing a leaked ghp_… token in a changed .mdnow failed=true (was: all checks skipped, passed).
    • Genuine docs-only PR (docs/note.md, no contribution dirs) → passes (convenience preserved).
    • Title set to $(touch /tmp/pwned) / backtick / "; echo failed=false >> $GITHUB_OUTPUT payloads → no file created, no injection executed, title treated as literal data.
    • Oversized (>1MB) file under docs/ with a [docs] title → failed=true (binary scan now runs in the docs path).
    • Empty recipe (README with no numbered steps, no code files) → fails category artifacts (Rule 6 fix).
  • Self-check: the workflow still ends with the Fail if checks failed step (exit 1 when failed == 'true'); the [docs] convenience is preserved for genuine docs-only PRs.

🤖 Generated with Claude Code

Fix four pre-existing weaknesses that defeated the PR gate's blocking
guarantee:

1. HIGH shell injection: PR title was interpolated raw via ${{ }} inside
   run: blocks (review + artifact steps). A crafted title could execute
   arbitrary shell in the runner and force failed=false. All untrusted
   PR-controlled values now pass through env: and are referenced as
   quoted shell variables.
2. HIGH [docs] bypass: any "[docs] ..." title exited 0 before any check
   ran, skipping the credential and binary scans regardless of changed
   files. Skip decision is now based solely on the actual changed-file
   list (no contribution folders touched), and the credential + binary
   scans always run and still block.
3. MEDIUM shallow fetch: base was fetched with --depth=1, which could
   lose the merge base and silently under-report changes. Base is now
   fetched in full, the diff fails closed (no last-commit-only fallback),
   and the merge base is verified.
4. LOW Rule 6 counting bug: grep -c || echo "0" could emit "0\n0" and
   break the integer comparison, letting empty recipes pass. Fixed with
   grep -c || true plus a default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

OB1 PR Gate

This PR does not touch contribution folders (docs/governance change). Contribution checks were skipped, but credential and binary-file scans still ran.

No credentials — No API keys, tokens, or secrets detected
No binary blobs — No oversized or binary files

Result: All 2 security scans passed.

@Humestone Humestone merged commit 2baca96 into main Jun 10, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant