PluginBench
Skill
Pass
Audit score 90

github-actions-hardening

github/awesome-copilot

Security hardening reviewer for GitHub Actions workflows — detects injection, privilege escalation, and supply-chain risks.

What is github-actions-hardening?

Analyzes GitHub Actions workflow files (.github/workflows/*.yml) for Actions-specific security threats that general linters miss: script injection via ${{ }} interpolation, privilege escalation from pull_request_target and workflow_run triggers, mutable action references, and over-scoped tokens. Use this when reviewing, auditing, or hardening any workflow, or when asked if a workflow is safe.

  • Detects script injection sinks where untrusted input (issue titles, PR bodies, branch names) flows into ${{ }} expressions and shell commands
  • Identifies privilege escalation risks from dangerous triggers (pull_request_target, workflow_run, issue_comment) that run with read/write tokens and secrets
  • Audits action references for supply-chain risk — flags third-party actions pinned to mutable tags/branches instead of commit SHAs
  • Reviews GITHUB_TOKEN permissions for over-scoping and recommends least-privilege per-job grants
  • Checks secret handling, GITHUB_ENV/GITHUB_OUTPUT injection, and self-hosted runner exposure on public repositories
  • Reasons about the Actions threat model and trust boundaries invisible to language linters

How to install github-actions-hardening

npx skills add https://github.com/github/awesome-copilot --skill github-actions-hardening
Claude Code
Cursor
Windsurf
Cline

How to use github-actions-hardening

  1. 1.Map the workflow triggers (on:) and classify privilege level — identify dangerous triggers like pull_request_target, workflow_run, and issue_comment that run with read/write tokens and secrets
  2. 2.Hunt for script injection by listing all ${{ }} expressions in run: blocks and actions/github-script steps, then check if any resolve to attacker-controllable event fields (issue.title, pull_request.body, comment.body, etc.)
  3. 3.Check that privileged triggers do not check out and execute untrusted code — flag pull_request_target or workflow_run workflows that run fork code as CRITICAL RCE risk
  4. 4.Audit the permissions: block — flag missing permissions (inherits repository default), recommend deny-all or minimal per-job scopes, and flag write-all or unnecessary write grants
  5. 5.Review all uses: action references — require third-party actions to be pinned to full 40-character commit SHAs, not tags or branches; flag @main, @master, or branch references as HIGH risk
  6. 6.Check secret and output handling — ensure no secrets are echoed, no set -x in secret-touching steps, and multiline user data uses heredoc delimiters when written to GITHUB_ENV or GITHUB_OUTPUT
  7. 7.Produce a findings report with severity summary table first, then grouped findings by issue type with exact file locations, offending YAML, plain-English risk explanation, and concrete before/after fixes

Use cases

Good for
  • Review a pull_request_target workflow to confirm it doesn't check out and execute fork code with a privileged token
  • Audit a workflow that uses issue_comment or workflow_run triggers to find injection sinks in user-controlled event fields
  • Pin third-party actions to commit SHAs and remove mutable tag/branch references to harden supply-chain security
  • Tighten GITHUB_TOKEN permissions from repository default (read/write all) to deny-all with minimal per-job grants
  • Secure a new workflow before merging by identifying ${{ }} injection risks and dangerous trigger/code combinations
Who it's for
  • DevOps engineers and platform teams hardening CI/CD pipelines
  • Security engineers auditing GitHub Actions workflows for compliance and threat modeling
  • Developers writing new workflows and wanting security-by-default guidance
  • Open-source maintainers securing workflows that accept external contributions

github-actions-hardening FAQ

Why is pull_request_target dangerous?

pull_request_target runs in the context of the base repository with a read/write token and full access to secrets, but can be triggered by a fork author. If the workflow checks out the fork's code (ref: ${{ github.event.pull_request.head.sha }}) and then runs it (build, test, npm install), that is remote code execution against a privileged token. The safe pattern is to split into two workflows: an unprivileged pull_request workflow that runs untrusted code, and a privileged workflow_run workflow that only consumes its results.

What is a script injection sink in GitHub Actions?

Any ${{ }} expression that resolves to attacker-controllable data and is placed in a run: block or script: input is a code-injection sink. The runner expands ${{ }} into the script before the shell executes it, so an issue titled "; <attacker-command> #" is pasted directly into your shell command. High-risk fields include github.event.issue.title, github.event.pull_request.body, github.event.comment.body, and any github.event.* field a fork author can set.

Why must third-party actions be pinned to commit SHAs?

Tags and branches are mutable. A compromised upstream action can rewrite v1 to malicious code that runs with your token and secrets. Pinning to a full 40-character commit SHA ensures the code you review is the code that runs. First-party actions (actions/*) are lower risk but SHA-pinning is still the hardened recommendation.

What should the permissions: block look like?

Start with a top-level permissions: {} (deny-all), then grant the minimum scope per job — for example, pull-requests: write only on the job that comments on PRs, contents: read only on jobs that need to read the repository. If there is no permissions: block, the workflow inherits the repository default, which may be read/write to everything.

How do I safely handle untrusted input in a workflow?

Never interpolate untrusted data directly into a run: command. Instead, pass it as an environment variable and reference it safely: set an env: variable from the untrusted field, then use $VAR (not ${{ }}). For multiline data written to GITHUB_ENV or GITHUB_OUTPUT, use the random-delimiter heredoc form to prevent injection of newlines and environment variables.

Full instructions (SKILL.md)

Source of truth, from github/awesome-copilot.


name: github-actions-hardening description: Security hardening reviewer for GitHub Actions workflow files (.github/workflows/*.yml). Reasons about the Actions threat model that pattern matchers and general code linters miss — untrusted-input script injection, privileged triggers running fork code, mutable action references, and over-scoped tokens. Use this skill when asked to review, audit, harden, or secure a GitHub Actions workflow, when writing a new workflow, or for any request like "is this workflow safe?", "review my CI for security issues", "why is pull_request_target dangerous here?", "pin my actions", or "lock down GITHUB_TOKEN permissions". Covers script injection via ${{ }} interpolation, pull_request_target / workflow_run privilege escalation, SHA-pinning of third-party actions, least-privilege permissions, GITHUB_ENV/GITHUB_OUTPUT injection, secret exposure, OIDC over long-lived credentials, and self-hosted runner exposure on public repositories.

GitHub Actions Hardening

A focused security reviewer for GitHub Actions workflows. It reasons about the Actions-specific threat model — where trust boundaries live in trigger types, token scopes, and string interpolation — rather than the application-code vulnerabilities a general security scanner looks for. Most workflow risks are invisible to language linters because the dangerous code is the YAML itself and the way GitHub expands ${{ }} expressions into a shell before your script runs.

When to Use This Skill

Use this skill when the request involves:

  • Reviewing, auditing, or hardening any file under .github/workflows/
  • Authoring a new workflow and wanting it secure by default
  • A workflow that uses pull_request_target, workflow_run, or issue_comment triggers
  • Questions about GITHUB_TOKEN permissions or the permissions: key
  • Pinning actions to commit SHAs vs tags vs branches
  • Handling untrusted input (issue titles, PR bodies, branch names, commit messages) in run: steps
  • OIDC / cloud authentication from Actions, or secret handling in CI
  • Self-hosted runners on public repositories
  • Any request like "is this workflow safe?", "secure my CI", or "review this GitHub Action"

The Core Insight

In a workflow, ${{ <expr> }} is expanded by the runner into the script before the shell executes it. So a step like:

- run: echo "Title: ${{ github.event.issue.title }}"

is not passing a variable — it is pasting attacker-controlled text directly into your shell command. An issue titled "; <attacker-command> # is concatenated into the script and executed. This single mechanism is the most common real-world Actions vulnerability, and models routinely generate it. Treat every ${{ }} that contains data an outside contributor can influence as a code-injection sink.

Execution Workflow

Follow these steps in order for every workflow reviewed.

Step 1 — Map the Triggers and Trust Level

Read every on: trigger and classify the workflow's privilege:

  • push, pull_request (from same repo) → runs with the contributor's own trust
  • pull_request from a fork → runs with a read-only token, no secrets (safe by design)
  • pull_request_target, workflow_run, issue_comment, issues → run in the context of the base repository with a read/write token and full access to secrets, but can be triggered by outside contributors. These are the dangerous triggers.

Read references/triggers-and-privilege.md for the full trust matrix.

Step 2 — Hunt for Script Injection

For every run: block, every script: in actions/github-script, and every input to a custom action, list the ${{ }} expressions and check whether any resolve to attacker-controllable data. High-risk contexts include:

  • github.event.issue.title, github.event.issue.body
  • github.event.pull_request.title, github.event.pull_request.body, .head.ref, .head.label
  • github.event.comment.body, github.event.review.body
  • github.event.pages.*.page_name, github.event.commits.*.message, github.event.head_commit.*
  • github.head_ref and any github.event.* field a fork author can set

Read references/injection.md for the complete sink list and the safe-pattern fixes.

Step 3 — Check Privileged Triggers Don't Execute Untrusted Code

If a pull_request_target or workflow_run workflow checks out PR/fork code (ref: ${{ github.event.pull_request.head.sha }}) and then runs it (build, test, install scripts, npm install with lifecycle scripts, etc.), that is remote code execution against a privileged token. Flag it as CRITICAL. The safe pattern is to split into two workflows: an unprivileged pull_request workflow that runs the untrusted code, and a privileged workflow_run workflow that only consumes its results.

Step 4 — Audit permissions:

  • If there is no permissions: block, the workflow inherits the repository default, which may be read/write to everything. Flag it.
  • Recommend a top-level permissions: {} (deny-all) or contents: read, then grant the minimum per job (e.g. pull-requests: write only on the job that comments).
  • Flag any permissions: write-all or broad write scopes that the steps don't actually need.

Read references/permissions-and-tokens.md for the per-scope guidance and OIDC setup.

Step 5 — Audit Action References (Supply Chain)

For every uses::

  • Third-party actions (not actions/* or github/*) MUST be pinned to a full 40-character commit SHA, not a tag or branch. Tags and branches are mutable; a compromised upstream action can rewrite v1 to malicious code that runs with your token and secrets.
  • First-party actions/* are lower risk but SHA-pinning is still the hardened recommendation.
  • Flag @main, @master, or any branch reference as HIGH — that is "latest" and can change under you at any time.
  • Note the human-readable version in a trailing comment: uses: foo/bar@<sha> # v2.1.0.

Read references/supply-chain.md for pinning, Dependabot for actions, and artifact/cache risks.

Step 6 — Check Secret and Output Handling

  • No secrets echoed, printed, or written to logs; no set -x / bash -x in steps that touch secrets.
  • Secrets must not be passed to steps that run untrusted code or to untrusted third-party actions.
  • Untrusted multiline data written to $GITHUB_ENV or $GITHUB_OUTPUT can inject environment variables or step outputs — use the random-delimiter heredoc form and never write raw user input.
  • actions/checkout leaves a token on disk by default; set persist-credentials: false when the job later runs untrusted code.

Step 7 — Produce the Report

Output findings using the format in references/report-format.md: a severity summary table first, then grouped findings with file, the exact offending YAML, the risk in plain English, and a concrete before/after fix. Never auto-apply changes — present them for review.

Severity Guide

SeverityMeaningExample
🔴 CRITICALToken/secret theft or RCE reachable by an outside contributorpull_request_target checking out and running fork code; ${{ github.event.* }} in a run: on a privileged trigger
🟠 HIGHExploitable supply-chain or scope problemThird-party action on a mutable tag/branch; write-all permissions; injection sink on issue_comment
🟡 MEDIUMRisk under conditions or chainingMissing permissions: block; secret reachable by a non-fork PR author
🔵 LOWHardening gap, low direct riskFirst-party action not SHA-pinned; persist-credentials left default on a non-privileged job
⚪ INFOObservation, not a vulnerabilityVersion comment missing next to a pinned SHA

Output Rules

  • Always show a findings summary table (counts by severity) first.
  • Group by issue type, not by file.
  • Be exact — quote the offending line and give the line location.
  • Always pair every CRITICAL/HIGH with a concrete corrected YAML snippet.
  • Never claim a fork pull_request is dangerous just because it runs untrusted code — it has no secrets and a read-only token. Reserve CRITICAL for the privileged triggers.
  • If the workflow is already hardened, say so and list what was checked.

Reference Files

Load these as needed:

  • references/triggers-and-privilege.md — Trust matrix for every trigger, why pull_request_target and workflow_run are privileged, and the two-workflow safe pattern.
    • Search patterns: pull_request_target, workflow_run, issue_comment, fork, secrets, read-only token, trust boundary
  • references/injection.md — Full list of attacker-controllable ${{ }} contexts and the env:-variable safe pattern for each sink (run, github-script, action inputs).
    • Search patterns: script injection, github.event, head_ref, issue title, env, intermediate variable, actions/github-script
  • references/permissions-and-tokens.md — GITHUB_TOKEN scopes, least-privilege permissions: recipes per job type, and OIDC for cloud auth instead of long-lived secrets.
    • Search patterns: permissions, GITHUB_TOKEN, write-all, contents: read, id-token, OIDC, least privilege
  • references/supply-chain.md — SHA-pinning third-party actions, Dependabot for github-actions, artifact and cache poisoning across workflow_run, and self-hosted runner exposure.
    • Search patterns: SHA pin, uses, mutable tag, Dependabot, download-artifact, cache, self-hosted runner
  • references/report-format.md — Output template: summary table, finding cards, and before/after remediation blocks.
    • Search patterns: report, format, finding, summary, remediation, before, after