Skip to content

Harden /merge workflow security#38

Closed
magicmark wants to merge 1 commit into
graphql:replace-codeowners-with-merge-botfrom
magicmark:fix/merge-security-hardening
Closed

Harden /merge workflow security#38
magicmark wants to merge 1 commit into
graphql:replace-codeowners-with-merge-botfrom
magicmark:fix/merge-security-hardening

Conversation

@magicmark
Copy link
Copy Markdown
Contributor

Summary

Security hardening for the /merge workflow introduced in #32.

Changes

  • Remove --auto flaggh pr merge --squash --auto enables auto-merge, which means the actual merge happens later (when checks pass). Between the /merge authorization check and the eventual merge, a PR author could force-push new commits that touch files outside their GAP directory (e.g. .github/workflows/). By merging immediately (without --auto), the authorization check and merge are atomic within the same workflow run.

  • Path traversal defense — Validate that all PR file paths are normalized (no .. components) and explicitly verify every file lives under the single authorized GAP directory. Git itself prevents .. in tree paths, so this is defense-in-depth rather than an active exploit, but it guards against any future API behavior changes.

  • Mergeable state check — Verify pr.mergeable before attempting merge to fail fast with a clear error message.

  • Filter out NONE association commenters — Skip workflow runs for commenters with no relationship to the repo. The authorization check would reject them anyway, but this avoids burning Actions minutes on spam.

  • Use npm ci with lockfile instead of ad-hoc npm install js-yaml — The original workflow installed js-yaml without version pinning at runtime, which is a supply-chain risk. The repo already has the yaml package in devDependencies with a lockfile, so we use that instead.

  • Convert to ESM — The project is "type": "module" and all other scripts use ESM. Aligns verify-merge.js with the rest of the codebase.

Security model after this change

  1. issue_comment triggers run the workflow from the default branch — an attacker cannot modify the verification logic via their PR
  2. Authorization is checked against metadata.yml on main (the checkout is the default branch)
  3. Merge happens immediately in the same step, so there's no window for the PR to change between check and merge
  4. Branch protection rules still apply (required checks must pass before gh pr merge succeeds)

Test plan

  • Comment /merge on a PR touching a single GAP dir as an authorized author → merges
  • Comment /merge as an unauthorized user → rejects with error
  • Comment /merge on a PR touching multiple GAP dirs → rejects
  • Comment /merge on a PR touching files outside gaps/ → rejects

🤖 Generated with Claude Code

- Remove --auto flag from gh pr merge to prevent race condition where
  PR contents could change between authorization check and merge
- Add path normalization check to reject traversal attempts
- Add explicit check that all files are within the authorized GAP dir
- Check PR mergeable state before attempting merge
- Filter out comments from users with no repo association to reduce
  Actions minute burn from spam
- Switch from ad-hoc js-yaml install to npm ci with lockfile (uses the
  yaml package already in devDependencies)
- Convert to ESM to match the rest of the project
- Parallelize API calls for faster execution
- Add per_page: 100 to listFiles to handle larger PRs
- Use async readFile instead of blocking readFileSync

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@magicmark magicmark force-pushed the fix/merge-security-hardening branch from 44edaaa to 607cad1 Compare May 20, 2026 00:09
@magicmark magicmark closed this May 20, 2026
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