Skip to content

Harden GitHub Actions workflows against token disclosure#6

Merged
SRWieZ merged 3 commits into
mainfrom
harden-workflows
May 18, 2026
Merged

Harden GitHub Actions workflows against token disclosure#6
SRWieZ merged 3 commits into
mainfrom
harden-workflows

Conversation

@SRWieZ

@SRWieZ SRWieZ commented May 18, 2026

Copy link
Copy Markdown
Member

Why

Composer CVE-2026-45793 (disclosed 2026-05-13) leaked GitHub Actions GITHUB_TOKEN values into stderr during a ~14-hour window when Composer's token-format validator rejected GitHub's new ghs_<id>_<base64url-JWT> token format and printed the rejected token verbatim. GitHub's secret masker couldn't reliably redact those tokens once Symfony Console wrapped or framed them with ANSI codes.

The audit of this repo found no workflow runs in the affected window (2026-05-12 → 2026-05-13), so no logs need scrubbing. This PR is forward-looking hardening, applying the pattern Laravel just merged in laravel/fortify#674.

What this PR changes

1. Pin every action to a commit SHA (with trailing version comment)

Tags are mutable — a maintainer-account compromise or a force-pushed @v2 tag (cf. tj-actions/changed-files, CVE-2025-30066) silently re-routes your workflow to attacker-controlled code on the next run. SHAs are immutable.

Action Pin
actions/checkout 34e114876b0b11c390a56381ad16ebd13914f8d5 (v4.3.1)
shivammathur/setup-php 7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc (v2.37.1)
actions/cache 0057852bfaa89a56745cba8c7296529d2fc39830 (v4.3.0)

2. Top-level permissions: contents: read on both workflows. No job needs more.

3. persist-credentials: false on every actions/checkout. No workflow pushes, so no credentials need to survive checkout.

4. Switch pint workflow to --test mode (was auto-commit)

Previously pint ran on every push and committed lint fixes back via stefanzweifel/git-auto-commit-action. That pattern has two drawbacks:

  • Every contributor PR gets an extra commit by the GITHUB_TOKEN identity — confusing for review, breaks signed-commit policies.
  • Persisting credentials on checkout (required by the auto-commit action) widens the blast radius of any later step.

The new pint workflow runs vendor/bin/pint --test, fails on diff, and asks contributors to run composer pint locally. The stefanzweifel/git-auto-commit-action dependency is dropped entirely (one less third-party action in the supply chain).

Trigger narrowed from "every push" to pull_request + push to main + workflow_dispatch to match test.yml.

5. .github/dependabot.yml so pinned SHAs don't rot

  • Monthly schedule (sufficient for the GitHub Actions ecosystem; security advisories file PRs immediately regardless of schedule).
  • Grouped (patterns: ["*"]) so all action bumps land in a single PR per month.
  • Labelled dependencies, ci for easy triage.

6. .github/CODEOWNERS@SRWieZ owns /.github/ so future workflow changes require maintainer review.

A note on the second commit (48e41a9)

When this branch was first pushed, the old auto-commit pint workflow was still in effect and ran one last time — it found a tiny pre-existing lint debt in cli/publicip.php (2 lines) and committed the fix as 48e41a9. That fix is legitimate. Subsequent pushes to this branch no longer auto-commit because the new pint triggers only on pull_request and push: main.

What this PR does not change

  • No major version bumps of any action — all pins are within the existing major. Dependabot can propose v5/v6/v7 bumps separately.
  • Composer version pinning: tools: composer:v2 already pulls latest 2.x (2.9.8+), which contains the CVE fix. No explicit pin needed.

Repo-level settings still to apply (separately, outside this PR)

  • Tag protection on v* so a stolen token cannot push a malicious release tag.
  • Branch protection on main requiring CODEOWNERS approval on .github/**.
  • Default GITHUB_TOKEN permissions at repo level set to read-only (forces every workflow to explicitly opt in to writes).

Happy to apply these via gh api after this PR merges.

Test plan

  • Tests workflow green on this PR (3 OS × 2 PHP = 6 cells).
  • Linting workflow green on this PR (Pint reports no diff after the auto-fix in 48e41a9).
  • After merge, Dependabot opens its first action-update PR on the first business day of next month.

References

SRWieZ and others added 3 commits May 18, 2026 14:50
Applies the hardening pattern from laravel/fortify#674, in response to
Composer CVE-2026-45793 (GitHub Actions GITHUB_TOKEN disclosure via
Composer error messages, May 12-13 2026).

- Pin every action to a commit SHA with a trailing version comment
- Add top-level permissions blocks (deny-all by default in pint.yml,
  contents:read in test.yml) with least-privilege per job
- Set persist-credentials: false on actions/checkout in test.yml.
  pint.yml keeps persist-credentials: true explicitly because
  git-auto-commit-action needs the token persisted to push lint fixes
- Add .github/dependabot.yml so pinned SHAs stay current (monthly,
  grouped, labelled)
- Add .github/CODEOWNERS so future .github/ changes require review
- Broaden pint.yml branches-ignore to dependabot/** (was npm_and_yarn
  only) so Dependabot PRs do not trigger an auto-commit back
Auto-committing lint fixes from CI has two drawbacks worth avoiding:
- Every contributor PR receives an extra commit attributed to the
  GITHUB_TOKEN identity, confusing review and breaking signed-commit
  policies.
- Persisting credentials on actions/checkout (required by the
  git-auto-commit-action) widens the blast radius of any later step.

Switching to `pint --test` makes the workflow fail on diff instead.
Contributors run `composer pint` locally before pushing.

- Drop the stefanzweifel/git-auto-commit-action dependency entirely
  (one less third-party action in the supply chain)
- Set persist-credentials: false on the checkout (no push needed)
- Drop the now-unused permissions: contents: write on the pint job;
  top-level permissions: contents: read is sufficient
- Trigger on pull_request + push to main only (matches test.yml)
- Drop the .env copy step (irrelevant for a library)
- Simplify CODEOWNERS to a single repo-rooted entry
@SRWieZ SRWieZ merged commit c582ca6 into main May 18, 2026
3 of 7 checks passed
SRWieZ added a commit to knotsphp/system that referenced this pull request May 18, 2026
- Pin every action to a commit SHA (with version comment)
- Top-level permissions: contents: read on both workflows
- persist-credentials: false on every actions/checkout
- Switch pint to --test mode; drop git-auto-commit-action dependency
- Add .github/dependabot.yml (monthly, grouped, labelled)
- Add .github/CODEOWNERS so future .github/ changes need review

Same pattern as knotsphp/publicip#6.
SRWieZ added a commit to knotsphp/flushdns that referenced this pull request May 18, 2026
- Pin every action to a commit SHA (with version comment)
- Top-level permissions: contents: read on both workflows
- persist-credentials: false on every actions/checkout
- Switch pint to --test mode; drop git-auto-commit-action dependency
- Add .github/dependabot.yml (monthly, grouped, labelled)
- Add .github/CODEOWNERS so future .github/ changes need review

Same pattern as knotsphp/publicip#6.
SRWieZ added a commit to SRWieZ/php-svg-ps-converter that referenced this pull request May 18, 2026
- Pin every action to a commit SHA (with version comment)
- Top-level permissions: contents: read on both workflows
- persist-credentials: false on every actions/checkout
- Switch pint to --test mode; drop git-auto-commit-action dependency
- Add .github/dependabot.yml (monthly, grouped, labelled)
- Add .github/CODEOWNERS so future .github/ changes need review

Same pattern as knotsphp/publicip#6.
SRWieZ added a commit to SRWieZ/queue-size-health-check that referenced this pull request May 18, 2026
- Pin every action to a commit SHA (with version comment)
- Top-level permissions: contents: read on both workflows
- persist-credentials: false on every actions/checkout
- Switch pint to --test mode; drop git-auto-commit-action dependency
- Add .github/dependabot.yml (monthly, grouped, labelled)
- Add .github/CODEOWNERS so future .github/ changes need review

Same pattern as knotsphp/publicip#6.
SRWieZ added a commit to SRWieZ/php-starlink-client that referenced this pull request May 18, 2026
- Pin every action to a commit SHA (with version comment)
- Top-level permissions: contents: read on both workflows
- persist-credentials: false on every actions/checkout
- Switch pint to --test mode; drop git-auto-commit-action dependency
- Add .github/dependabot.yml (monthly, grouped, labelled)
- Add .github/CODEOWNERS so future .github/ changes need review

Same pattern as knotsphp/publicip#6.
SRWieZ added a commit to SRWieZ/grpc-protoset that referenced this pull request May 19, 2026
- Pin every action to a commit SHA (with version comment)
- Top-level permissions: contents: read on both workflows
- persist-credentials: false on every actions/checkout
- Switch pint to --test mode; drop git-auto-commit-action dependency
- Add .github/dependabot.yml (monthly, grouped, labelled)
- Add .github/CODEOWNERS so future .github/ changes need review

Same pattern as knotsphp/publicip#6.
SRWieZ added a commit to SRWieZ/thumbhash that referenced this pull request May 19, 2026
* Harden GitHub Actions workflows against token disclosure

- Pin every action to a commit SHA (with version comment)
- Top-level permissions: contents: read
- persist-credentials: false on actions/checkout
- Add .github/dependabot.yml (monthly, grouped, labelled)
- Add .github/CODEOWNERS so future .github/ changes need review

Same pattern as knotsphp/publicip#6.

* Skip --display-deprecations on PHP 8.1

Pest resolves to a version using PHPUnit 9.x on PHP 8.1 (Pest 3.x
requires PHP 8.2+), and PHPUnit 9 does not support
--display-deprecations. Group PHP 8.1 with the no-flag run instead.
SRWieZ added a commit to SRWieZ/forge-heartbeats that referenced this pull request May 19, 2026
* Harden GitHub Actions workflows against token disclosure

- Pin every action to a commit SHA (with version comment)
- Top-level permissions: contents: read on tests.yml and static-analysis.yml;
  permissions: {} on update-changelog.yml (deny-all top, contents: write job)
- persist-credentials: false on every actions/checkout in tests.yml and
  static-analysis.yml; explicit persist-credentials: true on
  update-changelog.yml (git-auto-commit-action needs it to push CHANGELOG)
- Add .github/dependabot.yml (monthly, grouped, labelled)
- Add .github/CODEOWNERS so future .github/ changes need review

Same pattern as knotsphp/publicip#6.

* Drop Laravel 10 from test matrix

saloonphp/laravel-plugin ^4.0 (current composer.json constraint)
requires illuminate/console ^11.0, which is incompatible with Laravel 10.
Composer cannot resolve any Laravel 10 cell in the matrix; those jobs
have been red on main since 2026-03-30.

If Laravel 10 support is desired, loosen the saloonphp constraint in
composer.json — separate change.

* Revert "Drop Laravel 10 from test matrix"

This reverts commit 081b266.

* Support saloonphp v3 and v4

Widen the saloonphp/laravel-plugin and saloonphp/saloon constraints
to ^3.0 || ^4.0. This restores Laravel 10 support (saloon v4 dropped
Laravel 10) without forcing existing v3 users to upgrade.

All Saloon symbols used by this package (Connector, Request, Response,
SoloRequest, TokenAuthenticator, Method, HasBody, HasJsonBody,
AcceptsJson, RequestException) are stable across v3.0+ and v4.0+.

The CI matrix exercises both prefer-lowest (saloon v3) and prefer-stable
(saloon v4) across Laravel 10/11/12, so a regression on either major
will be caught.
SRWieZ added a commit to SRWieZ/php-bin-with-grpc that referenced this pull request May 19, 2026
- Pin every action to a commit SHA (with version comment)
- Top-level permissions: {} (deny-all) on both workflows; least-privilege
  job-level permissions
- build-php.yml: drop the top-level env: GITHUB_TOKEN block (over-broad,
  exposed the token to every step incl. third-party SPC binary).
  peter-evans/create-pull-request picks up GITHUB_TOKEN via its own
  token input default (github.token)
- update-ca-file.yml: replace deprecated/archived actions/create-release@v1
  with softprops/action-gh-release (SHA-pinned)
- persist-credentials: false on the build-php checkout (peter-evans uses
  its own token-based auth, not .git/config). update-ca-file.yml keeps
  persist-credentials: true explicitly because the workflow runs `git push`
  directly in shell to commit the CA file refresh
- Add .github/dependabot.yml (monthly, grouped, labelled)
- Add .github/CODEOWNERS so future .github/ changes need review

Same pattern as knotsphp/publicip#6.

Pre-existing bugs left untouched (worth a separate PR):
- update-ca-file.yml `release` job's `if:` references
  `needs.fetch-ca-file.outputs.changed`, but `fetch-ca-file` has no
  `outputs:` block — the conditional is always false, so the release
  job has never run.
- The same job references `steps.latestrelease.outputs.version`, but
  the curl in `latestrelease` assigns to a local bash var, never to
  GITHUB_OUTPUT.
SRWieZ added a commit to SRWieZ/svgtinyps-cli that referenced this pull request May 19, 2026
- Pin every action to a commit SHA (with version comment): checkout,
  cache, setup-php, upload-artifact, action-gh-release across all 8
  workflows
- Top-level permissions: {} (deny-all) on all 6 build-* workflows;
  contents: read on test.yml and pint.yml. Job-level contents: write
  only on the build jobs that need to upload release assets
- Drop the top-level env: GITHUB_TOKEN block from each build-* workflow
  (it exposed the token to every step, incl. the static-PHP build
  scripts). softprops/action-gh-release picks up github.token via its
  `token` input default
- persist-credentials: false on every actions/checkout (no workflow
  pushes to git; softprops uses GITHUB_TOKEN env via the REST API)
- pint.yml: switch from `composer pint` (fix mode, no-op in CI) to
  `vendor/bin/pint --test` so lint issues actually fail the build.
  Narrow trigger to pull_request + push to main (was every push minus
  one specific dependabot prefix)
- Add .github/dependabot.yml (monthly, grouped, labelled)
- Add .github/CODEOWNERS so future .github/ changes need review

Same pattern as knotsphp/publicip#6.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant