Audit Rules🔗
This page documents each of the audits currently implemented in zizmor.
See each audit's section for its scope, behavior, and other information.
Legend:
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow, Action, Dependabot | Links to vulnerable examples | Added to zizmor in this version | 
The audit works with --offline | 
The audit supports auto-fixes when used in the --fix mode | 
The audit supports custom configuration | 
anonymous-definition🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow, Action | N/A | v1.10.0 | ✅ | ❌ | ❌ | 
Detects workflows or action definitions that lack a name: field.
GitHub explicitly allows workflows to omit the name: field, and allows (but
doesn't document) the same for action definitions. When name: is omitted, the
workflow or action is rendered anonymously in the GitHub Actions UI, making it
harder to understand which definition is running.
Note
This is a --pedantic only audit, due to a lack of security impact.
Remediation🔗
Add a name: field to your workflow or action.
artipacked🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow | artipacked.yml | v0.1.0 | ✅ | ✅ | ❌ | 
Detects local filesystem git credential storage on GitHub Actions, as well as
potential avenues for unintentional persistence of credentials in artifacts.
By default, using actions/checkout causes a credential to be persisted
in the checked-out repo's .git/config, so that subsequent git operations
can be authenticated.
Subsequent steps may accidentally publicly persist .git/config, e.g. by
including it in a publicly accessible artifact via actions/upload-artifact.
However, even without this, persisting the credential in the .git/config
is non-ideal unless actually needed.
Other resources:
Remediation🔗
Unless needed for git operations, actions/checkout should be used with
persist-credentials: false.
If the persisted credential is needed, it should be made explicit
with persist-credentials: true.
bot-conditions🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow | bot-conditions.yml | v1.2.0 | ✅ | ✅ | ❌ | 
Detects potentially spoofable bot conditions.
Many workflows allow trustworthy bots (such as Dependabot)
to bypass checks or otherwise perform privileged actions. This is often done
with a github.actor check, e.g.:
However, this condition is spoofable: github.actor refers to the last actor
to perform an "action" on the triggering context, and not necessarily
the actor actually causing the trigger. An attacker can take
advantage of this discrepancy to create a PR where the HEAD commit
has github.actor == 'dependabot[bot]' but the rest of the branch history
contains attacker-controlled code, bypassing the actor check.
Other resources:
Remediation🔗
In general, checking a trigger's authenticity via github.actor is
insufficient. Instead, most users should use github.event.pull_request.user.login
or similar, since that context refers to the actor that created the Pull Request
rather than the last one to modify it.
More generally,
GitHub's documentation recommends
not using pull_request_target for auto-merge workflows.
Example
on: pull_request_target
jobs:
  automerge:
    runs-on: ubuntu-latest
    if: github.actor == 'dependabot[bot]' && github.repository == github.event.pull_request.head.repo.full_name
    steps:
      - run: gh pr merge --auto --merge "$PR_URL"
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
on: pull_request
jobs:
  automerge:
    runs-on: ubuntu-latest
    if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == github.event.pull_request.head.repo.full_name
    steps:
      - run: gh pr merge --auto --merge "$PR_URL"
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
cache-poisoning🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow | cache-poisoning.yml | v0.10.0 | ✅ | ✅ | ❌ | 
Detects potential cache-poisoning scenarios in release workflows.
Caching and restoring build state is a process eased by utilities provided by GitHub, in particular actions/cache and its "save" and "restore" sub-actions. In addition, many of the setup-like actions provided by GitHub come with built-in caching functionality, like actions/setup-node, actions/setup-java and others.
Furthermore, there are many examples of community-driven Actions with built-in caching functionality, like ruby/setup-ruby, astral-sh/setup-uv, Swatinem/rust-cache. In general, most of them build on top of actions/toolkit for the sake of easily integrate with GitHub cache server at Workflow runtime.
This vulnerability happens when release workflows leverage build state cached
from previous workflow executions, in general on top of the aforementioned
actions or  similar ones. The publication of artifacts usually happens driven
by trigger events like release or events with path filters like push
(e.g. for tags).
In such scenarios, an attacker with access to a valid GITHUB_TOKEN can use it
to poison the repository's GitHub Actions caches. That compounds with the
default behavior of actions/toolkit during cache restorations, allowing an
attacker to retrieve payloads from poisoned cache entries, hence achieving code
execution at Workflow runtime, potentially compromising ready-to-publish
artifacts.
Other resources:
- The Monsters in Your Build Cache – GitHub Actions Cache Poisoning
 - Cacheract: The Monster in your Build Cache
 
Remediation🔗
In general, you should avoid using previously cached CI state within workflows intended to publish build artifacts:
- Remove cache-aware actions like actions/cache from workflows that produce releases, or
 - Disable cache-aware actions with an 
if:condition based on the trigger at the step level, or - Set an action-specific input to disable cache restoration when appropriate,
  such as 
lookup-onlyin Swatinem/rust-cache. 
concurrency-limits🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow | concurrency-limits/ | v1.16.0 | ✅ | ❌ | ❌ | 
Detects insufficient concurrency limits in workflows.
By default, GitHub Actions allows multiple instances of the same workflow to run concurrently, even when the new runs fully supersede the old. This can be a resource waste vector for attackers, particularly on billed runners. Separately, it can be a source of subtle race conditions when attempting to locate artifacts by workflow and job identifiers, rather than run IDs.
Other resources:
Remediation🔗
Include a concurrency setting in your workflow that sets the
cancel-in-progress option either to true or to an expression that will be
true in most cases. Specifying false would allow separate instances of the
workflows to run concurrently, whereas true will imply that running jobs are
cancelled as soon as the workflow is re-triggered.
Example
dangerous-triggers🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow | pull-request-target.yml | v0.1.0 | ✅ | ❌ | ❌ | 
Detects fundamentally dangerous GitHub Actions workflow triggers.
Many of GitHub's workflow triggers are difficult to use securely. This audit checks for some of the biggest offenders:
pull_request_targetworkflow_run
These triggers are dangerous because they run in the context of the target repository rather than the fork repository, while also being typically triggerable by the latter. This can lead to attacker controlled code execution or unexpected action runs with context controlled by a malicious fork.
Many online resources suggest that pull_request_target and other
dangerous triggers can be used securely by ensuring that the PR's code
is not executed, but this is not true: an attacker can often find
ways to execute code in the context of the target repository, even if
the workflow doesn't explicitly run any code from the PR. Common vectors
for this include argument injection (e.g. with xargs), environment injection
(e.g. LD_PRELOAD), and local file inclusion (e.g. relinking files
to the runner's credentials file or similar).
Other resources:
- Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests
 - Keeping your GitHub Actions and workflows secure Part 4: New vulnerability patterns and mitigation strategies
 - Vulnerable GitHub Actions Workflows Part 1: Privilege Escalation Inside Your CI/CD Pipeline
 - Pwning the Entire Nix Ecosystem
 
Remediation🔗
The use of dangerous triggers can be difficult to remediate, since they don't always have an immediate replacement.
Replacing a dangerous trigger with a safer one (or keeping the dangerous trigger, but eliminating the risk of code execution) requires case-by-case consideration.
Some general pointers:
- Replace 
workflow_runtriggers withworkflow_call: this will require re-tooling the workflow to be a reusable workflow. - 
Replace
pull_request_targetwithpull_request, unless you absolutely need repository write permissions (e.g. to leave a comment or make other changes to the upstream repo).pull_request_targetis only needed to perform privileged actions on pull requests from external forks. If you only expect pull requests from branches within the same repository, or if you are fine with some functionality not working for external pull requests, preferpull_request. - 
Automation for Dependabot pull requests can be implemented using
pull_request, but requires setting dedicated Dependabot secrets and explicitly specifying needed permissions. - 
Never run PR-controlled code in the context of a
pull_request_target-triggered workflow. - 
Avoid attacker-controllable flows into
GITHUB_ENVin bothworkflow_runandpull_request_targetworkflows, since these can lead to arbitrary code execution. - 
If you really have to use
pull_request_target, consider adding a branch filter to only run the workflow for matching target branches.pull_request_targetuses the workflow file of the target branch of the pull request, therefore restricting the target branches reduces the risk of a vulnerablepull_request_targetin a stale or abandoned branch. - 
If you really have to use
pull_request_target, consider adding agithub.repository == ...check to only run for your repository but not in forks of your repository (in case the user has enabled Actions there). This avoids exposing forks to danger in case you fix a vulnerability in the workflow but the fork still contains an old vulnerable version.Important
Checking
github.repository == ...is not effective onworkflow_run, since aworkflow_runalways runs in the context of the target repository. 
dependabot-cooldown🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Dependabot | dependabot-cooldown/ | v1.15.0 | ✅ | ✅ | ❌ | 
Detects missing or insufficient cooldown settings in Dependabot configuration
files.
By default, Dependabot does not perform any "cooldown" on dependency updates. In other words, a regularly scheduled Dependabot run may perform an update on a dependency that was just released moments before the run began. This presents both stability and supply-chain security risks:
- Stability: updating to the newest version of a dependency immediately after its release increases the risk of breakage, since new releases may contain regressions or other issues that other users have not yet discovered.
 - Supply-chain security: package compromises are frequently opportunistic, meaning that the attacker expects to have their compromised version taken down by the packaging ecosystem relatively quickly. Updating immediately to a newly released version increases the risk of automatically pulling in a compromised version before it can be taken down.
 
To mitigate these risks, Dependabot supports per-updater cooldown settings.
However, these settings are not enabled by default; users must explicitly
enable them.
Other resources:
Remediation🔗
In general, you should enable cooldown for all updaters. The audit currently
enforces the following minimums:
default-days: must be at least4.
Example
dependabot-execution🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Dependabot | dependabot-execution/ | v1.15.0 | ✅ | ✅ | ❌ | 
Detects usages of insecure-external-code-execution in Dependabot configuration
files.
By default, Dependabot does not execution code from dependency manifests
during updates. However, users can opt in to this behavior by setting
insecure-external-code-execution: allow in their Dependabot
configuration.
Some ecosystems (including but not limited to Python, Ruby, and JavaScript) depend partially on code execution during dependency resolution.
In these ecosystems fully avoiding build-time code execution is impossible. However, build-time code execution should be avoided in automated dependency update contexts like Dependabot, since a compromised dependency may be able to obtain credentials or private source access automatically through a Dependabot job.
Other resources:
Remediation🔗
In general, automatic dependency updates should be limited to only updates that do not require code execution at resolution time.
In practice, this means that users should set
insecure-external-code-execution: deny or omit the field entirely
(and rely on the default of deny).
Example
excessive-permissions🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow | excessive-permissions.yml | v0.1.0 | ✅ | ❌ | ❌ | 
Detects excessive permissions in workflows, both at the workflow level and individual job levels.
Users frequently over-scope their workflow and job permissions, or set broad workflow-level permissions without realizing that all jobs inherit those permissions.
Furthermore, users often don't realize that the
default GITHUB_TOKEN permissions can be very broad,
meaning that workflows that don't configure any permissions at all can still
provide excessive credentials to their individual jobs.
Remediation🔗
In general, permissions should be declared as minimally as possible, and as close to their usage site as possible.
In practice, this means that workflows should almost always set
permissions: {} at the workflow level to disable all permissions
by default, and then set specific job-level permissions as needed.
Tip
GitHubSecurityLab/actions-permissions can help find the minimally required permissions.
Example
on:
  release:
    types:
      - published
name: release
permissions:
  id-token: write # trusted publishing + attestations
jobs:
  build:
    name: Build distributions 📦
    runs-on: ubuntu-latest
    steps:
      - # omitted for brevity
  publish:
    name: Publish Python 🐍 distributions 📦 to PyPI
    runs-on: ubuntu-latest
    needs: [build]
    steps:
      - name: Download distributions
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
        with:
          name: distributions
          path: dist/
      - name: publish
        uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
on:
  release:
    types:
      - published
name: release
permissions: {}
jobs:
  build:
    name: Build distributions 📦
    runs-on: ubuntu-latest
    steps:
      - # omitted for brevity
  publish:
    name: Publish Python 🐍 distributions 📦 to PyPI
    runs-on: ubuntu-latest
    needs: [build]
    permissions:
      id-token: write # trusted publishing + attestations
    steps:
      - name: Download distributions
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
        with:
          name: distributions
          path: dist/
      - name: publish
        uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
forbidden-uses🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow, Action | N/A | v1.6.0 | ✅ | ❌ | ✅ | 
An opt-in audit for denylisting/allowlisting specific uses: clauses.
This is not enabled by default; you must
configure it to use it.
Warning
This audit comes with several limitations that are important to understand:
- This audit is opt-in. You must configure it to use it; it does nothing by default.
 - This audit (currently) operates on repository 
uses:clauses, e.g.uses: actions/checkout@v4. It does not operate on Dockeruses:clauses, e.g.uses: docker://ubuntu:24.04. This limitation may be lifted in the future. - This audit operates on 
uses:clauses as they appear in the workflow and action files. In other words, in cannot detect impostor commits or indirect usage of actions via manualgit cloneand local path usage. - This audit's configuration operates on patterns, just like
  unpinned-uses. That means that you can't (yet)
  define exact matches. For example, you can't forbid 
actions/checkout@v4, you have to forbidactions/checkout, which forbids all versions. 
Configuration🔗
rules.forbidden-uses.config.<allow|deny>🔗
Type: list
The forbidden-uses audit operates on either an allowlist or denylist
basis:
- 
In allowlist mode, only the listed
uses:patterns are allowed. All non-matchinguses:clauses result in a finding.Intended use case: only allowing "known good" actions to be used, and forbidding everything else.
 - 
In denylist mode, only the listed
uses:patterns are disallowed. All matchinguses:clauses result in a finding.Intended use case: permitting all
uses:by default, but explicitly forbidding "known bad" actions. 
Regardless of the mode, the patterns used are repository patterns. See Configuration - Repository patterns for details.
Example
The following configuration would allow only actions owned by the @actions organization, plus any actions defined in github/codeql-action:
Example
The following would allow all actions except for those in the @actions organization or defined in github/codeql-action:
Remediation🔗
Either remove the offending uses: clause or, if intended, add it to
your configuration.
github-env🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow, Action | github-env.yml | v0.6.0 | ✅ | ❌ | ❌ | 
Detects dangerous writes to the GITHUB_ENV and GITHUB_PATH environment variables.
When used in workflows with dangerous triggers (such as pull_request_target and workflow_run),
GITHUB_ENV and GITHUB_PATH can be an arbitrary code execution risk:
- If the attacker is able to set arbitrary variables or variable contents via
  
GITHUB_ENV, they may be able to setLD_PRELOADor otherwise induce code execution implicitly within subsequent steps. - If the attacker is able to add an arbitrary directory to the 
$PATHviaGITHUB_PATH, they may be able to execute arbitrary code by shadowing ordinary system executables (such asssh). 
Other resources:
- GitHub Actions exploitation: environment manipulation
 - GHSL-2024-177: Environment Variable injection in an Actions workflow of Litestar
 - Google & Apache Found Vulnerable to GitHub Environment Injection
 - Hacking with Environment Variables
 
Remediation🔗
In general, you should avoid modifying GITHUB_ENV and GITHUB_PATH within
sensitive workflows that are attacker-triggered, like pull_request_target.
If you absolutely must use GITHUB_ENV or GITHUB_PATH, avoid passing
attacker-controlled values into either. Stick with literal strings and
values computed solely from trusted sources.
If you need to pass state between steps, consider using GITHUB_OUTPUT instead.
hardcoded-container-credentials🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow | hardcoded-credentials.yml | v0.1.0 | ✅ | ❌ | ❌ | 
Detects Docker credentials (usernames and passwords) hardcoded in various places within workflows.
Remediation🔗
Use encrypted secrets instead of hardcoded credentials.
Example
on:
  push:
jobs:
  test:
    runs-on: ubuntu-latest
    container:
      image: fake.example.com/example
      credentials:
        username: user
        password: hackme
    services:
      service-1:
        image: fake.example.com/anotherexample
        credentials:
          username: user
          password: hackme
    steps:
      - run: echo 'hello!'
on:
  push:
jobs:
  test:
    runs-on: ubuntu-latest
    container:
      image: fake.example.com/example
      credentials:
        username: user
        password: ${{ secrets.REGISTRY_PASSWORD }}
    services:
      service-1:
        image: fake.example.com/anotherexample
        credentials:
          username: user
          password: ${{ secrets.REGISTRY_PASSWORD }} # (1)!
    steps:
      - run: echo 'hello!'
- This may or may not be the same credential as above, depending on your configuration.
 
impostor-commit🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow, Action | impostor-commit.yml | v0.1.0 | ❌ | ✅ | ❌ | 
Detects commits within a repository action's network that are not present on the repository itself, also known as "impostor" commits.
GitHub represents a repository and its forks as a "network" of commits.
This results in ambiguity about where a commit comes from: a commit
that exists only in a fork can be referenced via its parent's
owner/repo slug, and vice versa.
GitHub's network-of-forks design can be used to obscure a commit's true origin
in a fully-pinned uses: workflow reference. This can be used by an attacker
to surreptitiously introduce a backdoored action into a victim's workflows(s).
A notable historical example of this is github/dmca@565ece4, which appears to be on github/dmca is but really on a fork (with an impersonated commit author).
Other resources:
Remediation🔗
Impostor commits are visually indistinguishable from normal best-practice hash-pinned actions.
Always carefully review external PRs that add or change hash-pinned actions by consulting the claimant repository and confirming that the commit actually exists within it.
The only remediation, once discovered, is to replace the impostor commit within an authentic commit (or an authentic tag/branch reference).
insecure-commands🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow, Action | insecure-commands.yml | v0.5.0 | ✅ | ✅ | ❌ | 
Detects opt-in for executing insecure workflow commands.
Workflow commands (like ::set-env and ::add-path)
were deprecated by GitHub in 2020 due to their inherent weaknesses
(e.g., allowing any command with the ability to emit to stdout
to inject environment variables and therefore obtain code execution).
However, users can explicitly re-enable them by setting the
ACTIONS_ALLOW_UNSECURE_COMMANDS environment variable at the workflow,
job, or step level.
Other resources:
Remediation🔗
In general, users should use GitHub Actions environment files
(like GITHUB_PATH and GITHUB_OUTPUT) instead of using workflow commands.
Example
known-vulnerable-actions🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow, Action | known-vulnerable-actions.yml | v0.1.0 | ❌ | ✅ | ❌ | 
Detects actions with known, publicly disclosed vulnerabilities that are tracked in the GitHub Advisories database. Examples of commonly disclosed vulnerabilities in GitHub Actions include credential disclosure and code injection via template injection.
Remediation🔗
If the vulnerability is applicable to your use: upgrade to a fixed version of the action if one is available, or remove the action's usage entirely.
obfuscation🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow, Action | N/A | v1.7.0 | ✅ | ✅ | ❌ | 
Checks for obfuscated usages of GitHub Actions features.
This audit primarily serves to "unstick" other audits, which may fail to detect functioning but obfuscated usages of GitHub Actions features.
This audit detects a variety of obfuscated usages, including:
- Obfuscated paths within 
uses:clauses, including redundant/separators and uses of.or..in path segments. - Obfuscated GitHub expressions, including no-op patterns like
  
fromJSON(toJSON(...))and calls toformat(...)where all arguments are literal values. 
Remediation🔗
Address the source of obfuscation by simplifying the expression,
uses: clause, or other obfuscated feature.
Example
overprovisioned-secrets🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow, Action | overprovisioned-secrets.yml | v1.3.0 | ✅ | ❌ | ❌ | 
Detects excessive sharing of the secrets context.
Typically, users access the secrets context via its individual members:
This allows the Actions runner to only expose the secrets actually used by the workflow to the job environment.
However, if the user instead accesses the entire secrets context:
...then the entire secrets context is exposed to the runner, even if
only a single secret is actually needed.
Remediation🔗
In general, users should avoid loading the entire secrets context.
Secrets should be accessed individually by name.
Example
ref-confusion🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow, Action | ref-confusion.yml | v0.1.0 | ❌ | ❌ | ❌ | 
Detects actions that are pinned to confusable symbolic refs (i.e. branches or tags).
Like with impostor commits, actions that are used with a symbolic ref
in their uses: are subject to a degree of ambiguity: a ref like
@v1 might refer to either a branch or tag ref.
An attacker can exploit this ambiguity to publish a branch or tag ref that takes precedence over a legitimate one, delivering a malicious action to pre-existing consumers of that action without having to modify those consumers.
Remediation🔗
Switch to hash-pinned actions.
ref-version-mismatch🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow, Action | ref-version-mismatch.yml | v1.14.0 | ✅ | ✅ | ❌ | 
Detects uses: clauses where the action is hash-pinned, but the associated
tag comment (used by tools like Dependabot) does not match the pinned commit.
This can happen innocently when a user (or automation) updates a
hash-pinned uses: clause to a newer commit, but fails to update the
associated tag comment. When this happens, tools like Dependabot will silently
ignore the comment instead of refreshing it on subsequent updates, making
it progressively more out-of-date over time.
Remediation🔗
Update the tag comment to match the pinned commit. Tools like suzuki-shunsuke/pinact may be able to do this automatically for you.
Example
secrets-inherit🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow | secrets-inherit.yml | v1.1.0 | ✅ | ❌ | ❌ | 
Detects excessive secret inheritance between calling workflows and reusable (called) workflows.
Reusable workflows can be given secrets by their calling workflow either
explicitly, or in a blanket fashion with secrets: inherit. The latter
should almost never be used, as it makes it violates the
Principle of Least Authority and makes it impossible to determine which exact
secrets a reusable workflow was executed with.
Remediation🔗
In general, secrets: inherit should be replaced with a secrets: block
that explicitly forwards each secret actually needed by the reusable workflow.
Example
self-hosted-runner🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow | self-hosted.yml | v0.1.0 | ✅ | ❌ | ❌ | 
Note
This is a --pedantic only audit, due to zizmor's limited ability
to analyze runner configurations themselves. See #34 for more details.
Detects self-hosted runner usage within workflows.
GitHub supports self-hosted runners, which behave similarly to GitHub-hosted runners but use client-managed compute resources.
Self-hosted runners are very hard to secure by default, which is why GitHub does not recommend their use in public repositories.
Other resources:
Remediation🔗
In general, self-hosted runners should only be used on private repositories. Exposing self-hosted runners to potential public use is always a security risk.
In practice, there are many cases (such as custom host configurations) where a self-hosted runner is needed on a public repository. In these cases, there are steps you can take to minimize their risk:
- Require manual approval on workflows for all external contributors. This can be configured at repository, workflow, or enterprise-wide levels. See GitHub's docs for more information.
 - Use only ephemeral ("just-in-time") runners. These runners are created just-in-time to perform one job and are destroyed immediately afterwards, making it harder (but not impossible) for an attacker to maintain persistence.
 
stale-action-refs🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow, Action | N/A | v1.7.0 | ❌ | ❌ | ❌ | 
Checks for uses: clauses which pin an action using a SHA reference,
but where that reference does not point to a Git tag.
When using an action commit which is not a Git tag / release version, that commit might contain bugs or vulnerabilities which have not been publicly documented because they might have been fixed before the subsequent release. Additionally, because changelogs are usually only published for releases, it is difficult to tell which changes of the subsequent release the pinned commit includes.
Note
This is a --pedantic only audit because the detected situation is not
a vulnerability per se. But it might be worth investigating which commit
the SHA reference points to, and why not a SHA reference pointing to a
Git tag is used.
Some action repositories use a "rolling release branch" strategy where all commits on a certain branch are considered releases. In such a case findings of this audit can likely be ignored.
Remediation🔗
Change the uses: clause to pin the action using a SHA reference
which points to a Git tag.
template-injection🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow, Action | template-injection.yml | v0.1.0 | ✅ | ✅ | ❌ | 
Detects potential sources of code injection via template expansion.
GitHub Actions allows workflows to define template expansions, which
occur within special ${{ ... }} delimiters. These expansions happen
before workflow and job execution, meaning the expansion
of a given expression appears verbatim in whatever context it was performed in.
Template expansions aren't syntax-aware, meaning that they can result in
unintended shell injection vectors. This is especially true when they're
used with attacker-controllable expression contexts, such as
github.event.issue.title (which the attacker can fully control by supplying
a new issue title).
Tip
When used with a "pedantic" or "auditor" persona, this audit will flag all template expansions in code contexts, even ones that are likely safe.
This is because zizmor considers all template expansions in code contexts
to be code smells, and attempting to selectively permit them is more
error-prone than forbidding them in a blanket fashion.
Other resources:
Remediation🔗
The most common forms of template injection are in run: and similar
code-execution blocks. In these cases, an inline template expansion
can typically be replaced by an environment variable whose value comes
from the expanded template.
This avoids the vulnerability, since variable expansion is subject to normal shell quoting/expansion rules.
Tip
To fully remediate the vulnerability, you should not use
${{ env.VARNAME }}, since that is still a template expansion.
Instead, you should use ${VARNAME} to ensure that the shell itself
performs the variable expansion.
Tip
When switching to ${VARNAME}, keep in mind that different shells have
different environment variable syntaxes. In particular, Powershell (the
default shell on Windows runners) uses ${env:VARNAME}.
To avoid having to specialize your handling for different runners,
you can set shell: sh or shell: bash.
Example
undocumented-permissions🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow | undocumented-permissions.yml | v1.13.0 | ✅ | ❌ | ❌ | 
Detects explicit permissions blocks that lack explanatory comments.
This audit recommends adding comments to document the purpose of each permission in explicit permissions blocks. Well-documented permissions help prevent over-scoping and make workflows more maintainable by explaining why specific permissions are needed.
The audit does not flag contents: read, as this is a common, self-explanatory
permission.
Note
This is a --pedantic only audit, as it focuses on code quality and
maintainability rather than security vulnerabilities.
Remediation🔗
Add inline comments explaining why each permission is needed:
unpinned-images🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow, Action | unpinned-images.yml | v1.7.0 | ✅ | ❌ | ❌ | 
Checks for container.image values where the image is not pinned by either a tag (other than latest) or SHA256.
When image references are unpinned or are pinned to a mutable tag, the workflow is at risk because the image used will be unpredictable over time. Changes made to the OCI registry used to source the image may result in untrusted images gaining access to your workflow.
This can be a security risk:
- Registries may not consistently enforce immutable image tags
 - Completely unpinned images can be changed at any time by the OCI registry.
 
By default, this audit applies the following policy:
- 
Regular findings are created for all image references missing a tag
or using the
latesttag: - 
Pedantic findings are created for all image references using a tag (
!= latest) rather than SHA256 hash. 
Other resources:
- Aqua: The Challenges of Uniquely Identifying Your Images
 - GitHub: Safeguard your containers with new container signing capability in GitHub Actions
 
Remediation🔗
Pin the container.image: value to a specific SHA256 image registry hash.
Many popular registries will display the hash value in their web console or you
can use the command line to determine the hash of an image you have previously pulled
by running docker inspect redis:7.4.3 --format='{{.RepoDigests}}'.
Example
unpinned-uses🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow, Action | unpinned.yml | v0.4.0 | ✅ | ❌ | ✅ | 
Detects "unpinned" uses: clauses.
When a uses: clause is not pinned by branch, tag, or SHA reference,
GitHub Actions will use the latest commit on the referenced repository's
default branch (or, in the case of Docker actions, the :latest tag).
Similarly, if a uses: clause is pinned via branch or tag (i.e. a "symbolic
reference") instead of a SHA reference, GitHub Actions will use whatever
commit is at the tip of that branch or tag. GitHub does not have immutable
branches or tags, meaning that the action can change without the symbolic
reference changing.
This can be a security risk:
- Completely unpinned actions can be changed at any time by the upstream repository.
 - Tag- or branch-pinned actions can be changed by the upstream repository, either by force-pushing over the tag or updating the branch.
 
If the upstream repository is trusted, then symbolic references are often suitable. However, if the upstream repository is not trusted, then actions should be pinned by SHA reference.
By default, this audit applies the following policy:
- Official GitHub actions namespaces can be pinned by branch or tag.
  In other words, 
actions/checkout@v4is acceptable. - All other actions must be pinned by SHA reference.
 
This audit can be configured with a custom set of rules, e.g. to
allow symbolic references for trusted repositories or entire namespaces
(e.g. foocorp/*). See
unpinned-uses - Configuration for details.
Specifying a configuration overrides the default policy above.
Other resources:
Configuration🔗
Note
unpinned-uses is configurable in v1.6.0 and later.
If the default unpinned-uses rules isn't suitable for your use case,
you can override it with a custom set of policies.
rules.unpinned-uses.config.policies🔗
Type: object
The rules.unpinned-uses.config.policies object defines your unpinned-uses
policies.
Each member is a pattern: policy rule, where pattern describes which
uses: clauses to match and policy describes how to treat them.
The pattern is a repository pattern; see
Configuration - Repository patterns
for details.
The valid policies are:
hash-pin: anyuses:clauses that match the associated pattern must be fully pinned by SHA reference.ref-pin: anyuses:clauses that match the associated pattern must be pinned either symbolic or SHA reference.- 
any: no pinning is required for anyuses:clauses that match the associated pattern.Tip
For repository
usesclauses likeuses: actions/checkout@v4this is equivalent toref-pin, as GitHub Actions does not permit completely unpinned repository actions. 
If a uses: clauses matches multiple rules, the most specific one is used
regardless of definition order.
Example
The following configuration contains two rules that could match actions/checkout, but the first one is more specific and therefore gets applied:
In plain English, this policy set says "anything that uses: actions/* must
be at least ref-pinned, but actions/checkout in particular must be hash-pinned."
Example
In plain English, this policy set says "anything that uses: example/* must
be hash-pinned, and anything else must be at least ref-pinned."
Important
If a uses: clause does not match any rules, then an implicit
"*": hash-pin rule is applied. Users can override this implicit rule
by adding their own * rule or a more precise rule, e.g.
"github/*": ref-pin for actions under the @github organization.
Remediation🔗
Tip
There are several third-party tools that can automatically hash-pin your workflows and actions for you:
- suzuki-shunsuke/pinact: supports updating and hash-pinning workflows, actions, and arbitrary inputs.
 - davidism/gha-update: supports updating and hash-pinning workflow definitions.
 - 
stacklok/frizbee: supports hash-pinning (but not updating) workflow definitions.
See also stacklok/frizbee#184 for current usage caveats.
 
For repository actions (like actions/checkout): add a branch, tag, or SHA reference.
For Docker actions (like docker://ubuntu): add an appropriate
:{version} suffix.
Example
unredacted-secrets🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow, Action | unredacted-secrets.yml | v1.4.0 | ✅ | ❌ | ❌ | 
Detects potential secret leakage via redaction failures.
Typically, users access the secrets context via its individual members:
This allows the Actions runner to redact the secret values from the job logs, as it knows the exact string value of each secret.
However, if the user instead treats the secret as a structured value, e.g. JSON:
...then the password field is not redacted, as the runner does not
treat arbitrary substrings of secrets as secret values.
Other resources:
Remediation🔗
In general, users should avoid treating secrets as structured values. For example, instead of storing a JSON object in a secret, store the individual fields as separate secrets.
Example
unsound-condition🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow, Action | unsound-condition.yml | v1.12.0 | ✅ | ✅ | ❌ | 
Detects conditions that are inadvertently always true despite containing an expression that should control the evaluation.
A common source of these is an unintentional interaction
between multi-line YAML strings and fenced GitHub Actions expressions.
For example, the following condition always evaluates to true, despite
appearing to evaluate to false:
This happens because YAML's "block" scalars include a trailing newline
by default, which is left outside of the GitHub Actions expression.
This results in an expansion like 'false\n' instead of 'false',
which GitHub Actions interprets as a truthy value.
Remediation🔗
There are two ways to remediate this:
- 
Avoid fenced expressions in
if:conditions. Instead, write the expression as a "bare" expression.This will still include the trailing newline, but it will be inside of the expression as seen from the GitHub Actions expression parser.
 - 
Use fenced expressions, but use a YAML block scalar that does not include a trailing newline. Either
|-or>-is appropriate for this purpose. 
unsound-contains🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow | unsound-contains.yml | v1.7.0 | ✅ | ❌ | ❌ | 
Detects conditions that use the contains() function in a way that can be bypassed.
Some workflows use contains() to check if a context variable is in a list of
values (e.g., if the the push that triggered the job targeted a certain
branch), and then bypass checks or otherwise perform privileged actions:
However, this condition will not only evaluate to true if either
refs/heads/main or refs/heads/develop is passed, but also for substrings of
those values. For example, if someone pushes to a branch named mai, then
github.ref would contain the string refs/heads/mai and the job would also
execute.
Remediation🔗
To check if a value is contained in a list of strings, the first argument to
contains() should be an actual list, not a string. This can be done by using
the fromJSON() function:
Alternatively, it's possible to check for equality individually and combine the results using the logical "or" operator:
Other resources:
Example
use-trusted-publishing🔗
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable | 
|---|---|---|---|---|---|
| Workflow | pypi-manual-credential.yml | v0.1.0 | ✅ | ❌ | ❌ | 
Detects packaging workflows that could use Trusted Publishing.
Some packaging ecosystems/indices (like PyPI and RubyGems) support "Trusted Publishing," which is an OIDC-based "tokenless" authentication mechanism for uploading to the index from within a CI/CD workflow.
This "tokenless" flow has significant security benefits over a traditional manually configured API token, and should be preferred wherever supported and possible.
Other resources:
- Trusted Publishers for All Package Repositories
 - Trusted publishing: a new benchmark for packaging security
 
Remediation🔗
In general, enabling Trusted Publishing requires a one-time change to your package's configuration on its associated index (e.g. PyPI or RubyGems).
Each ecosystem has its own resources for using a Trusted Publisher once it's configured:
- 
Python (PyPI)
Usage: pypa/gh-action-pypi-publish
 - 
Ruby (RubyGems)
Usage: rubygems/release-gem
 - 
Rust (crates.io)
Usage: rust-lang/crates-io-auth-action.
 - 
Dart (pub.dev)
 - 
JavaScript (npm)