We hardened zizmor's GitHub Actions static analyzer
In March 2026, attackers exploited a pull_request_target misconfiguration in
the aquasecurity/trivy-action GitHub Action to exfiltrate organization and
repository secrets, then used those credentials to backdoor LiteLLM on PyPI (see
Trivy’s post-mortem for the full timeline). zizmor is a static analyzer
that GitHub Actions users run to catch exactly these misconfigurations before they ship.
When GitHub Actions added support for YAML anchors in September 2025, a small but
high-value slice of the ecosystem started writing workflows that zizmor could only
analyze on a best-effort basis.
Over the past three months, Trail of Bits collaborated with the zizmor maintainers
to bring zizmor’s anchor support up to full coverage. First, we fixed parsing bugs
that caused crashes, produced wrong-location findings, and silently mishandled aliased values.
Second, we surfaced deserialization edge cases that broke zizmor on otherwise valid workflows.
Finally, we helped align zizmor’s expression evaluator with GitHub’s own
Known Answer Tests. We validated all of this against a new corpus of 41,253 workflows
from 6,612 high-value open-source repositories. The result: 20 filed issues, 15 merged pull
requests.
Building the test corpus
To understand how anchors are used in CI today and to stress-test zizmor
against the full variety of YAML it encounters in the wild, we built a corpus
of real workflows. We used BigQuery’s GitHub dataset to identify the 10,000
most-starred repositories created between 2022 and 2025, filtered to the 6,612
that use GitHub Actions, and downloaded every workflow file. That gave us
41,253 YAML files.

When we ran zizmor against the corpus, it crashed on 45 of the 41,253
workflows. That’s a low rate, but each crash means a bug in zizmor.
How anchors are used in the wild
zizmor’s anchor support was deliberately limited, and for good reason.
YAML anchors make workflows non-local: an alias defined in one place changes
behavior elsewhere in the file. This complicated zizmor’s parsing model, and
adoption was rare enough that the zizmor maintainers reasonably discouraged
anchor use. In our corpus, only 43 of the 41,253 workflows use YAML anchors (roughly 0.1%), but those 43 include some of the most foundational projects in open source:
However, anchors are a supported feature, and their use will likely grow over time.
We found two common patterns. The first is reusing steps across jobs, as Bitcoin Core’s CI does:
jobs:
runners:
steps:
- &ANNOTATION_PR_NUMBER
name: Annotate with pull request number
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "::notice ..."
fi
test-each-commit:
steps:
- *ANNOTATION_PR_NUMBER
- uses: actions/checkout@v6The second pattern is pinning action versions once. For instance, Home Assistant’s CI defines the action reference (with its SHA hash) using an anchor, then reuses it wherever the same action appears:
jobs:
lint:
steps:
- uses: &actions-setup-python actions/setup-python@a309ff8b42...
# later in the same workflow:
- uses: *actions-setup-pythonFour anchor handling bugs found and fixed
When we started, four anchor patterns from these workflows broke zizmor.
Aliases in sequences were incorrectly flattened. When a YAML alias appeared
inside a sequence (like a list of steps), zizmor’s internal path representation
spread the alias contents rather than treating it as a single element. This
caused zizmor to crash or produce findings pointing at the wrong location
in the file. (Fixed in #1557)
Anchor prefixes leaked into values.
foo: [&name v, *x]In YAML flow sequences, anchor prefixes like &name weren’t stripped from
resolved values. Given the snippet in Figure 4, looking up the first element of
foo would return &name v instead of v, causing any step that consumed the
node value to fail. (Fixed in #1562)
Duplicate anchors caused a crash. The YAML spec allows redefining an anchor
name (the last definition wins). zizmor’s YAML layer assumed anchor names were
unique and panicked on duplicates. (Fixed in #1575)
The template-injection audit crashed on aliased run values. When a
YAML alias was used as a scalar run: value, the audit didn’t expect the
indirection and failed. (Fixed in #1732)
To prevent future regressions, we also added integration tests covering anchor patterns found in real workflows (#1682) and updated the anchor documentation (#1788).
What else the corpus surfaced
Running zizmor against the full test corpus also surfaced bugs that had nothing to
do with anchors.
Deserialization edge cases. GitHub Actions accepts YAML constructs that
zizmor’s workflow model didn’t anticipate: if: 0 (an integer where a string
is expected), timeout-minutes: 0.5 (a float where an integer is expected),
secrets: inherit (a string where a mapping is expected). Each one caused
zizmor to reject the entire workflow. We reported these as individual issues
(#1670, #1672, #1674), and the maintainers fixed them quickly.
Expression evaluator bugs. zizmor evaluates GitHub Actions expressions to
determine whether user-controlled data flows into dangerous sinks. We validated
the evaluator against GitHub’s own Known Answer Tests and helped the
maintainers align zizmor’s behavior with the official test suite (#1694).
Upstream issues. We also traced some crashes to bugs in an upstream dependency, tree-sitter-yaml, and filed issues and PRs there (tree-sitter-yaml#39, tree-sitter-yaml#43). Even the YAML 1.2 test suite doesn’t cover every edge case the spec permits.
Securing CI where it matters most
Supply-chain attacks like the Trivy compromise begin with a single
misconfigured workflow. GitHub Actions is by far the most popular CI system
for open-source projects, and zizmor plays an important role in helping
maintainers catch risky configurations before attackers do.
By gathering 41,253 real-world workflows and running zizmor against all of
them, we tested its robustness against the full variety of YAML patterns that
projects actually use. We fixed several anchor-handling bugs, reported
deserialization and expression-evaluator issues, and broadened the set of
workflows zizmor can analyze cleanly. The methodology is straightforward:
download real inputs, run the tool, triage the failures. Any static analysis
tool can benefit from the same approach.
We’d like to thank the zizmor maintainers, in particular
@woodruffw, for their responsiveness and
thorough code review throughout this work. We’d also like to thank the
Sovereign Tech Agency, whose vision for
OSS security and funding made this work possible.
