Harden the GitHub Actions workflows - #856
Open
2ndkauboy wants to merge 2 commits into
Open
Conversation
Three measures from the same review, all of them things the workflows here did
not do at all.
Every `actions/checkout` now sets `persist-credentials: false`. By default the
action leaves a usable `GITHUB_TOKEN` in `.git/config` for the rest of the job,
including the jobs that go on to run `composer install` and `npm install` — that
is, to execute third-party code with a credential sitting on disk beside it. No
workflow pushes with git, and the steps that do call the API pass `GH_TOKEN`
themselves, so nothing depended on the persisted copy.
Seven workflows had no `permissions:` block and therefore ran at whatever the
repository default is. They now declare what they need: `contents: read` for the
test and static-analysis workflows and for the two that publish to wordpress.org
(those authenticate against SVN with their own credentials and never touch the
GitHub API), and an empty block in `spam-detection-comparison.yml`, whose only
job already declares its own.
The remaining template expressions inside `run:` blocks now travel through the
environment instead. An expression is expanded before the shell ever sees the
line, so whatever it expands to is executed as code rather than read as an
argument. The one that mattered is `composer install ${{ inputs.composer-flags }}`
in `.github/actions/setup-php/action.yml`, which every PHP job in the repository
runs through; `wordpress-plugin-check.yml`, `attach-snapshot.yml` and
`spam-detection-comparison.yml` had smaller ones.
The previous commit fixed thirty findings that nobody had a way of seeing. This adds the thing that would have seen them, so the next one does not need a blog post to be noticed: a `Workflow hardening` job in the static-analysis workflow running zizmor over `.github/`. The plain CLI rather than `zizmorcore/zizmor-action` — one fewer third-party action in the workflow that argues for fewer of them, and the action's SARIF upload would want `security-events: write` for a report that reads just as well in the log. It gets the read-only job token only so the online checks work; the one that matters is whether a pinned hash is really a commit in the action's own repository, which is how an impostor pin gets caught. The path filter widens from this one workflow and the shared PHP setup to all of `.github/workflows/**` and `.github/actions/**`, since the new job has an opinion about every file there. That leaves `dependabot-cooldown` on all three ecosystems in `dependabot.yml`. It is a real question — a cooldown trades a few days of update latency for immunity to a release that is hijacked and then yanked — but it is a question about dependency policy rather than about workflows, and it is answered in #855. Suppressed here with the reasoning written down, so the job reports green on everything that this pull request is actually about.
✅ WordPress Plugin Check Report
📊 ReportAll checks passed! No errors or warnings found. 🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Prompted by Sebastian Bergmann's Hardening GitHub Actions Workflows, which walks through the pass he did over PHPUnit's workflows with zizmor.
Measured against that article, most of the work was already done here: every
uses:is a commit SHA with a version comment, Dependabot advances those pins, there is nopull_request_target, noworkflow_runand noissue_commentanywhere, and the release workflows already pass event data throughenv:rather than splicing it into shell. This closes the two measures the repository had not adopted at all, plus the injection sites that were left.zizmor goes from 30 findings to 0 (1 high, 25 medium, 1 low, 1 informational → none).
persist-credentials: falseon every checkoutNot set anywhere before, so all twelve checkouts left a usable
GITHUB_TOKENin.git/configfor the rest of the job — including the jobs that then runcomposer installandnpm install, which is to say execute third-party code with a credential sitting on disk next to it.Nothing depended on it: no workflow pushes with git, and every step that calls the API already sets
GH_TOKENitself.Least-privilege
permissions:Seven workflows declared none and therefore ran at whatever the repository default is. The two that matter most are
wordpress-plugin-deploy.ymlandwordpress-plugin-asset-update.yml, which hand real wordpress.org SVN credentials to a third-party action.contents: readfor the four test/static-analysis workflows and the two wordpress.org ones — they authenticate against SVN with their own credentials and never touch the GitHub API.permissions: {}inspam-detection-comparison.yml, whose single job already declares its own.Event data through the environment
An expression is expanded before the shell ever sees the line, so what it expands to runs as code rather than being read as an argument.
The one that mattered is
composer install ${{ inputs.composer-flags }}in.github/actions/setup-php/action.yml— zizmor's only high-severity finding, and every PHP job in the repository goes through it. Deliberately$COMPOSER_FLAGSunquoted: the flags are several words and have to split into several arguments, and word splitting is all that ever happens to an environment variable.Smaller ones in
wordpress-plugin-check.yml(github.event.repository.name, now bound once as a job-levelPLUGIN_SLUG),attach-snapshot.ymlandspam-detection-comparison.yml.A
Workflow hardeningjobSo the next regression does not need a blog post to be noticed. Plain
pipx run zizmorrather thanzizmorcore/zizmor-action: one fewer third-party action in the workflow that argues for fewer of them, and the action's SARIF upload would wantsecurity-events: writefor a report that reads just as well in the log. The read-only job token is passed only to lift zizmor out of offline mode — the check it enables is whether a pinned hash is really a commit in the action's own repository, which is how an impostor pin gets caught.The path filter widens accordingly, from this workflow and
.github/actions/setup-php/**to all of.github/workflows/**and.github/actions/**.Deliberately not here
dependabot-cooldownon all three ecosystems — a real question, but one about dependency policy rather than workflows. Suppressed inline with the reasoning written down, and tracked in Consider a Dependabot cooldown for Composer, npm and GitHub Actions updates #855.gh" advice does not transfer: there is no built-in equivalent for the wordpress.org SVN dance. They are SHA-pinned and Dependabot moves them, which is the part that was actually load-bearing.Verification
zizmor --persona=regular .github/reports zero findings, online and offline. The release-time workflows cannot be exercised from a pull request; their diffs are the same two mechanical changes as everywhere else, and each API-calling step still setsGH_TOKENexplicitly.