Skip to content

fix(ci): harden privileged workflows - #1003

Open
bokelley wants to merge 1 commit into
mainfrom
codex/security-workflow-hardening
Open

fix(ci): harden privileged workflows#1003
bokelley wants to merge 1 commit into
mainfrom
codex/security-workflow-hardening

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

  • pin third-party actions to immutable commits
  • validate privileged trigger payloads and collaborator permissions
  • remove shell interpolation of untrusted issue and dispatch content
  • execute IPR/import tooling only from immutable trusted code checkouts

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

  • actionlint passes for all three changed workflows
  • action SHAs verified against their release tags
  • independent workflow-security review against current origin/main

Compatibility

Privileged PR-comment and repository-dispatch paths now require write, maintain, or admin collaborator permission.

@aao-ipr-bot aao-ipr-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 to env: in the ctx step and referenced as a shell var — no ${{ }} lands inside a run: program. security-reviewer: sound.
  • args.all cannot forge a commenter= line. The args GITHUB_OUTPUT write uses a args-$(openssl rand -hex 16) heredoc delimiter (128 bits, unguessable); number/commenter/comment_id are regex-validated to single tokens before their plain echo. 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 via issue_comment on a PR or repository_dispatch, and the Authorize step's if: is exactly those two. issues events never resolve to a PR. No ungated path to a mutation.
  • ipr-agreement.yml closes the pwn-request pattern. PR-head is never checked out. Executable code comes from adcontextprotocol/adcp@82a6716 (immutable, github.token, persist-credentials:false); the app token touches only the main ledger checkout (persist-credentials:true) for the write-back. check-and-record.mjs and its siblings import only node: builtins + relative modules — no npm ci needed. concurrency serializes ledger writes.
  • sync-agent-roles.yml stops executing mutable upstream code. import-claude-agents.mjs now runs from the reviewed local copy; the upstream tarball supplies .agents/roles as data only, and drift lands as a human-reviewed PR. No mutable upstream JS runs under the write token.
  • head -c ... || true fixes a latent bug. Under set -euo pipefail, head closing the pipe at the cap sends SIGPIPE upstream (141), which pipefail+set -e would 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.repository on the react steps is equivalent to the old client_payload value on the only path that runs them (kind == 'manual', same-repo) and strictly more robust.

Open question (what flips this to approve)

  1. Does collaborators/{user}/permission succeed with a contents: read GITHUB_TOKEN? The Authorize step (claude-issue-triage.yml) calls gh api repos/$REPO/collaborators/$COMMENTER/permission with GH_TOKEN: ${{ github.token }}, but the job grants only contents: read. GitHub documents this endpoint as requiring push access. If the restricted token 403s, the command-substitution fails under set -euo pipefail and 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 for TRIAGE_DISPATCH_PAT instead of github.token — a hint the default token's scope is tight here. Confirm with one live /triage on 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.yml still trusts upstream main for 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)

  1. head -c ... || true masks non-SIGPIPE failures. claude-issue-triage.yml:144,172 — the || true also swallows a genuine tr/printf error, silently yielding an empty body. Worst case is an under-informed routine, so fine to ship, but a PIPESTATUS/exit-141 check would be tighter.
  2. Pin comments track mutable tags. The # vX.Y.Z next 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.

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