fix(ci): harden privileged workflows - #1003
Open
bokelley wants to merge 1 commit into
Open
Conversation
Contributor
There was a problem hiding this comment.
Solid hardening across all three privileged workflows — the security model is right. Untrusted event fields no longer touch shell programs, pull_request_target runs trusted pinned code instead of PR-head, and mutation gates fail closed. One open question on the new authz gate holds it at a comment rather than an approve, and it fails safe either way.
Things I checked
- Expression-injection removal is complete. Every
github.event.*/client_payload.*field is bound toenv:in thectxstep and referenced as a shell var — no${{ }}lands inside arun:program.security-reviewer: sound. args.allcannot forge acommenter=line. TheargsGITHUB_OUTPUT write uses aargs-$(openssl rand -hex 16)heredoc delimiter (128 bits, unguessable);number/commenter/comment_idare regex-validated to single tokens before their plainecho. Breakout not reachable.- The mutation surface is fully gated.
is_pr=true(PR-feedback mode pushes a commit to the head branch) is only reachable viaissue_commenton a PR orrepository_dispatch, and the Authorize step'sif:is exactly those two.issuesevents never resolve to a PR. No ungated path to a mutation. ipr-agreement.ymlcloses the pwn-request pattern. PR-head is never checked out. Executable code comes fromadcontextprotocol/adcp@82a6716(immutable,github.token,persist-credentials:false); the app token touches only themainledger checkout (persist-credentials:true) for the write-back.check-and-record.mjsand its siblings import onlynode:builtins + relative modules — nonpm cineeded.concurrencyserializes ledger writes.sync-agent-roles.ymlstops executing mutable upstream code.import-claude-agents.mjsnow runs from the reviewed local copy; the upstream tarball supplies.agents/rolesas data only, and drift lands as a human-reviewed PR. No mutable upstream JS runs under the write token.head -c ... || truefixes a latent bug. Underset -euo pipefail,headclosing the pipe at the cap sends SIGPIPE upstream (141), whichpipefail+set -ewould surface as a step failure once the writer blocks on a full pipe buffer. Real fix for large issue bodies.- All five action pins match their release tags;
github.repositoryon the react steps is equivalent to the oldclient_payloadvalue on the only path that runs them (kind == 'manual', same-repo) and strictly more robust.
Open question (what flips this to approve)
- Does
collaborators/{user}/permissionsucceed with acontents: readGITHUB_TOKEN? The Authorize step (claude-issue-triage.yml) callsgh api repos/$REPO/collaborators/$COMMENTER/permissionwithGH_TOKEN: ${{ github.token }}, but the job grants onlycontents: read. GitHub documents this endpoint as requiring push access. If the restricted token 403s, the command-substitution fails underset -euo pipefailand the step exits 1 — which fail-closes and blocks every mutation-capable trigger, including legitimate maintainers, so the gate this PR adds would be dead on arrival for everyone. This fails safe (no security hole, no data risk — hence a comment, not a block), but it kills the feature. The two react steps already reach forTRIAGE_DISPATCH_PATinstead ofgithub.token— a hint the default token's scope is tight here. Confirm with one live/triageon a PR (or a docs cite), and if it 403s, either grant the needed scope or use the PAT for the check. That's the one thing that flips this to approve.
Follow-ups (non-blocking — file as issues)
- Validation in the PR body covers
actionlint, SHA verification, and a security review — but not a live run of the new authz gate under the restricted token. The gate ships unvalidated against its own runtime path; the smoke test above closes that. sync-agent-roles.ymlstill trusts upstreammainfor role content. Acceptable residual (drift is human-reviewed before merge), but worth a one-line note that synced role prose isn't trusted until reviewed.
Minor nits (non-blocking)
head -c ... || truemasks non-SIGPIPE failures.claude-issue-triage.yml:144,172— the|| truealso swallows a genuinetr/printferror, silently yielding an empty body. Worst case is an under-informed routine, so fine to ship, but aPIPESTATUS/exit-141 check would be tighter.- Pin comments track mutable tags. The
# vX.Y.Znext to each SHA is advisory only; keep the "verify SHA against tag in a dedicated PR" discipline the header comments already describe.
LGTM after the collaborator-permission smoke test confirms the gate lets maintainers through.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Why
Privileged workflows trusted mutable action/code references and placed attacker-controlled event fields into shell contexts. Author association alone was also insufficient authorization for sensitive triggers.
Validation
actionlintpasses for all three changed workflowsorigin/mainCompatibility
Privileged PR-comment and repository-dispatch paths now require write, maintain, or admin collaborator permission.