A green GitHub Actions badge tells you the workflow completed. It does not prove that the workflow's token permissions, event triggers, dependencies, or execution environment are safe.
I built a small, free, browser-only pattern checker for a few of these risks. In building and testing it, I found five questions worth asking during every CI configuration review.
All YAML below is synthetic. I'm not claiming a vulnerability in a real third-party repository.
1. What can GITHUB_TOKEN actually write?
Start with the smallest permission set that lets a job do its work.
This deserves extra scrutiny:
name: CI
on: [push, pull_request]
permissions: write-all
For a job that only reads code and runs checks, a narrower starting point is:
name: CI
on: [push, pull_request]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo "Replace with your actual tests"
Be aware that organization, repository, workflow and job configuration can affect effective permissions. Finding the text write-all is a review prompt, not a complete permissions audit.
2. Are action versions pinned to immutable commits?
A reference such as actions/checkout@v4 uses a version tag rather than a full commit ID. Tags may move. Pinning a reviewed action revision to its full commit SHA limits unexpected changes in dependencies:
# Version-tagged reference: review the supply-chain tradeoff.
- uses: actions/checkout@v4
# Template only: obtain the actual reviewed 40-character SHA.
- uses: actions/checkout@<REVIEWED_40_CHARACTER_COMMIT_SHA>
Do not copy the placeholder into a real workflow. Pinning is not code verification: you still need to assess what the action does and keep it patched.
3. Does pull_request_target interact with untrusted code?
pull_request_target can be legitimate for labeling or triaging PRs. The risk is combining a privileged base-repository context with checking out and running code from an untrusted PR.
For example, examine this event carefully:
on:
pull_request_target:
Ask: Does the job check out a fork's head commit? Run an arbitrary PR script? Access secrets? Could it use pull_request without elevated privileges instead?
Treat the combination of event context, checked-out content and token scope as one security decision. The trigger name by itself does not prove exploitability.
4. Can issue/PR text reach a shell command?
Issue titles, PR bodies and other event fields are untrusted. Directly interpolating them into run: can allow shell interpretation of data that was supposed to remain inert.
A risky pattern to review:
# Untrusted event text inserted directly into the shell program:
- run: echo "${{ github.event.pull_request.title }}"
A safer direction is to pass untrusted input as data, for example through an environment variable, and quote the variable when consumed:
- name: Print PR title
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: printf '%s\n' "$PR_TITLE"
This doesn't eliminate every possible risk. Always evaluate the actual shell, quoting, data flow and capabilities.
5. Where does a self-hosted runner execute?
Self-hosted runners have a different trust boundary from managed ephemeral build environments. An untrusted PR job could expose cached files, network routes or secrets if isolation and cleanup are insufficient.
Check whether runs-on: self-hosted is intentional, whether jobs share persistent state and whether a fork can trigger a privileged workflow. An automated text scan cannot decide whether your runner's real isolation is safe.
Try an actual local-only preview
I built a free GitHub Actions YAML pattern preview. Paste a nonsecret YAML or select a file you are authorized to use and click the free button.
The local browser preview checks a few common patterns without uploading YAML. It is deliberately a heuristic: false positives and missed problems are possible.
For developers who want a more detailed machine-readable report rather than a text scan, the same page offers an optional, one-time 0.20 USDC/Base x402 static report with findings and SARIF from our already-running service. It requires the buyer's explicit wallet approval and only sends their authorized YAML after that approval. This is not a security certification, and I have not yet independently confirmed a real purchase through this new browser checkout. The free tool requires no wallet.
I tested the checkout with 59 offline security/unit checks, two fake-wallet browser flows and real unsigned HTTP 402 price quotes on desktop/mobile. Mocked signatures and quote checks are not evidence that a customer paid.
Open the more detailed free five-step checklist, including the current official references:
- GitHub Actions secure use reference
- GitHub Actions script-injection concepts
- Securely using
pull_request_target
Disclosure: This is an original educational post from a one-owner, AI-assisted VM5 development workflow. The example checks are public, source is here, and no outside-client audit, bounty award, user count or paid security report is being claimed.
Top comments (0)