Your GitHub Token Is Leaking Through Composer Pins in setup-php
A subtle interaction between setup-php and pinned Composer versions can expose your GitHub tokens to dependency mirrors. Here's how it works, who's affected, and what to do right now.
Your CI Secrets Might Be Sitting in Composer's Config — Exposed to the Internet
If you're running PHP CI pipelines on GitHub Actions — and let's be honest, most PHP shops are — there's a disclosure that dropped yesterday that deserves your immediate attention. It's not a flashy RCE. It's not a zero-day in PHP itself. It's worse in some ways: it's a quiet credential leak that could have been silently exposing your GitHub tokens to third-party mirrors every time your pipeline runs.
The advisory is GHSA-5wxr-w449-57cm, published on 2026-05-20. It affects a very specific — but surprisingly common — pattern: pinning an exact Composer version through the setup-php GitHub Action. If you've ever written tools: composer:2.9.7 in your workflow file, you need to keep reading.
What makes this particularly insidious is that it's not a bug in your code. It's not even a bug in Composer per se. It's an interaction between how setup-php configures authentication and how certain Composer versions handle that configuration when resolving packages. The result? Your GITHUB_TOKEN — or worse, a PAT with broader permissions — gets sent to places it was never meant to go.
What Happened
The setup-php action is one of the most popular GitHub Actions for PHP development, maintained by Shivam Mathur. It handles PHP version management, extension installation, and critically, tool installation — including Composer. When you configure setup-php with a GitHub token (which most people do for private repo access or to avoid rate limits), it stores that token in Composer's auth.json configuration.
The problem emerges with specific Composer versions that have a bug in how they handle authentication tokens during package resolution. When Composer resolves dependencies, it may send configured authentication headers to mirrors or alternative download URLs — not just to the primary repository endpoint. In affected versions, the token configured by setup-php leaks to these third-party endpoints.
This only triggers when you pin an exact affected Composer semver version through setup-php. If you're using the default Composer version (which setup-php keeps updated), or if you're using a version range, you're likely fine. The affected versions are specific Composer 2.x releases where this token-forwarding behavior was introduced as a regression.
The key detail: workflows using tools: composer (no version pin) get the latest stable Composer, which has already been patched. It's only the explicit pins like tools: composer:2.9.7 or similar affected versions that create the exposure window.
The discovery timeline suggests this was found through behavioral analysis of Composer's HTTP requests during CI runs. Someone noticed auth headers showing up in requests to CDN mirrors that shouldn't have received them. That's the kind of subtle leak that can persist for months before anyone notices.
Technical Deep-Dive: How the Token Leaks
Let's walk through exactly what happens. When setup-php runs with a GitHub token configured, it sets up Composer's authentication like this:
{
"github-oauth": {
"github.com": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}This gets written to Composer's auth.json. Under normal circumstances, Composer should only send this token to github.com and api.github.com endpoints. The security boundary is clear: credentials for domain X should never be sent to domain Y.
Here's where the affected Composer versions break down. When resolving package downloads, Composer follows redirects from GitHub's API to actual download URLs. These download URLs might point to codeload.github.com, CDN endpoints, or in the case of mirrors configured in composer.json, entirely different domains.
The vulnerable pattern in the affected Composer versions looks conceptually like this:
// Simplified representation of the vulnerable behavior
public function downloadFile($url, $options) {
// Auth headers are attached based on the ORIGINAL request domain
$headers = $this->getAuthHeaders($url);
// But when following redirects or hitting mirrors...
$response = $this->httpClient->get($url, ['headers' => $headers]);
if ($response->isRedirect()) {
// BUG: Auth headers from original domain carried to redirect target
return $this->downloadFile($response->getRedirectUrl(), $options);
}
}The root cause is a failure to strip authentication headers when the request crosses a domain boundary. This violates a fundamental HTTP security principle: credentials are scoped to origins. When you follow a redirect from api.github.com to objects.githubusercontent.com or a third-party mirror, the auth headers should be dropped.
This is the same class of bug that has hit curl, wget, and countless HTTP client libraries over the years. It's CWE-200 (Exposure of Sensitive Information) combined with improper credential scoping on redirects.
The attack scenario becomes concrete when you consider Composer mirror configurations. A composer.json might include:
{
"repositories": [
{
"type": "composer",
"url": "https://packagist-mirror.example.com"
}
]
}If an attacker controls or can observe traffic to that mirror, they receive your GitHub token in the Authorization header. Even without a malicious mirror, legitimate CDN endpoints that handle package downloads would receive tokens they shouldn't have — expanding the attack surface for any compromise of those CDN providers.
The particularly nasty edge case: if your workflow uses a Personal Access Token (PAT) instead of the default GITHUB_TOKEN, the blast radius expands dramatically. The default GITHUB_TOKEN is scoped to the current repository and expires after the workflow run. A PAT might have repo scope across your entire organization.
How to Detect If You're Affected
First, check your workflow files for pinned Composer versions:
# Search all workflow files for pinned composer versions
grep -rn "composer:2\." .github/workflows/
# More specific: find exact version pins (not ranges)
grep -rn "tools:.*composer:[0-9]\+\.[0-9]\+\.[0-9]\+" .github/workflows/If you get hits, check whether the pinned version is in the affected range. The advisory specifies the affected versions — cross-reference your pin against those.
Next, audit your Composer configuration for mirrors or alternative repositories that could have received leaked tokens:
# Check for non-default repositories in composer.json
jq '.repositories[]? | select(.url | test("packagist.org") | not)' composer.json
# Check if auth.json exists and what domains it covers
cat ~/.composer/auth.json 2>/dev/null | jq 'keys'For a more thorough check, you can add a temporary debug step to your CI pipeline to see what Composer is actually sending:
- name: Debug Composer HTTP traffic
run: |
# Enable verbose output to see where auth headers go
composer install -vvv 2>&1 | grep -i "authorization\|token\|auth" || trueWarning: Only run this in a private repository's CI. The verbose output might itself expose token prefixes in logs. Remove this step immediately after testing.
If you find you've been running an affected version, assume the token has been exposed. Rotate it immediately — don't wait to confirm exploitation.
Impact Analysis
The blast radius here is narrower than it first appears, but for those affected, it's serious. You need three conditions to be vulnerable:
- Using
setup-phpwith a GitHub token configured - Pinning an exact affected Composer version (not using the default)
- Having package resolution that hits non-GitHub endpoints (mirrors, redirects to CDNs, private registries)
The third condition is almost always true — Composer's package downloads regularly involve redirects to CDN endpoints. So really, it comes down to conditions 1 and 2.
For affected workflows, an attacker who can observe traffic to any of those redirect targets gets a valid GitHub token. With the default GITHUB_TOKEN, they get temporary write access to the repository where the workflow ran. With a PAT, they potentially get access to every private repository the token owner can reach. That's source code theft, supply chain injection, secret extraction from other repos — the full menu.
Chain attacks are the real concern. A leaked token with packages:write scope could be used to publish malicious package versions. A token with actions:write could modify workflow files to establish persistence. The window might be short (workflow tokens expire), but automated exploitation can move fast.
What to Do Right Now
The fix is straightforward. Stop pinning affected Composer versions. Here's your migration path:
Option 1: Use the default (recommended)
# Before (vulnerable)
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:2.9.7
# After (safe — uses latest stable)
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composerThis is the simplest fix. The setup-php action maintains Composer at a patched version by default.
Option 2: Pin to a fixed version
If you need version pinning for reproducibility (valid reason), pin to a version that includes the fix:
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:2.9.8 # Or whatever patched version is specified in the advisoryOption 3: Restrict token permissions
Regardless of which option you choose, tighten your workflow permissions:
permissions:
contents: read
packages: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Use default token, not a PATAfter applying the fix, verify it's working by checking that Composer's auth configuration is properly scoped:
# In your CI pipeline, verify auth.json only targets github.com
composer config --global --list 2>/dev/null | grep -i authIf you were using a PAT and suspect exposure, rotate it immediately through GitHub Settings → Developer Settings → Personal Access Tokens. Also audit your token's usage logs in GitHub's security log for any unexpected API calls.
The Bigger Picture: CI Credential Hygiene Is Still Terrible
This vulnerability is part of a recurring pattern I keep seeing: CI/CD systems that handle credentials with insufficient isolation. We saw it with the tj-actions/changed-files compromise earlier in 2025. We saw it with the codecov bash uploader incident. The theme is always the same — tokens get configured broadly and then leak through unexpected channels.
The lesson for anyone building CI pipelines: treat every tool in your pipeline as a potential credential exfiltration vector. Pin your actions to commit SHAs, not tags. Use the most restrictive token permissions possible. Prefer the default GITHUB_TOKEN over PATs. And if you must use a PAT, create a fine-grained one scoped to exactly what's needed.
For the PHP ecosystem specifically, this is a reminder that Composer's security model assumes you trust every endpoint it talks to. That assumption breaks down in CI environments where tokens are configured globally. The Composer team has been improving this — token scoping, domain-restricted auth — but the defaults still lean toward convenience over security. Until that changes, the burden is on us to configure things defensively.
Related posts
- Security
How I got free cinema credit by ordering -2 popcorns
A missing input validation on M-Tix Cinema XXI's food ordering API let me increase my account balance by submitting negative quantities. No tools needed — just a browser.
May 19, 2026 · 6 min - Security
How I analyze API security headers in 30 seconds
A quick checklist for reading HTTP response headers and spotting security misconfigurations before you even look at the response body.
May 18, 2026 · 7 min - Security
Common auth mistakes I find when reverse-engineering APIs
After years of poking at APIs that weren't meant to be poked at, these are the auth patterns that break most often — and why.
May 18, 2026 · 9 min