1. Limit the GITHUB_TOKEN to the permissions the job needs
The workflow token can be capable of modifying repository resources, depending on configuration and context. Start with the least privilege needed. Avoid broad permissions: write-all unless there is an explicit, well-reviewed business reason. Job-specific permissions can tighten access further.
# Illustrative least-privilege example
name: Verify
on: [push, pull_request]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo "Replace with approved tests"
A local string match for write-all is only a quick reminder: effective permissions may come from workflow, job, repository and organization settings. Human review must check the actual running context.
2. Check action references and third-party code
A version tag like actions/checkout@v4 may move. Pinning a reviewed action to its full commit SHA makes the referenced action version harder to change unexpectedly. Review dependencies and follow trusted update practices; a SHA does not independently prove code quality or absence of malicious logic.
# Illustration ONLY; replace the placeholder before use - uses: actions/checkout@<REVIEWED_40_CHARACTER_COMMIT_SHA> # Do not copy this placeholder into a live workflow.
Pay attention to third-party actions that request elevated permissions, upload artifacts, or read secrets. Dependabot or Renovate can help maintain known reviewed pins.
3. Treat pull_request_target as an elevated-trust trigger
GitHub's pull_request_target event uses the base repository's trusted workflow code and may access elevated privileges. It is not intrinsically unsafe: the danger appears when the privileged workflow subsequently checks out or executes untrusted pull-request code. Check whether a job fetches a fork's head commit, untrusted artifact, or dependency before running build/test steps with secrets.
# A trigger to REVIEW, not a standalone vulnerability: on: pull_request_target: # Safer design depends on the task: # Use pull_request when a privileged event is unnecessary. # Keep untrusted PR code isolated from privileged jobs.
GitHub maintains a specific secure use guide for pull_request_target describing default protections and the problematic combination of untrusted code execution plus privileged access.
4. Avoid interpreting untrusted event strings as shell code
Issue titles, pull-request bodies, branch names and similar fields can contain attacker-controlled strings. Substituting those directly into an inline run: script can result in shell injection, even when the YAML itself looks valid. Prefer treating user text as data through environment variables and quoted parameters, and follow GitHub's documented defensive patterns.
# Review any direct interpolation from an untrusted event:
# run: echo "${{ github.event.pull_request.title }}"
#
# Prefer environment/data separation, for example:
env:
PR_TITLE: ${{ github.event.pull_request.title }}
# Then inspect how a subsequent approved script quotes $PR_TITLE.
This is a security-review reminder rather than an exploitation demonstration. Whether an actual attack is possible depends on the surrounding shell, escaping and privileges. See GitHub's script-injection documentation.
5. Restrict self-hosted runners and privileged secrets
A self-hosted runner may retain state between jobs and can expose internal resources. In particular, running untrusted pull-request code on a non-isolated self-hosted runner is high risk. Use isolated ephemeral infrastructure for sensitive workloads, ensure network restrictions and examine which secrets are available. Public-repository contributions deserve extra care.
This site’s local checker flags the presence of self-hosted only as a prompt. It cannot assess your runtime isolation, firewall, secret management or effective trust boundary.