Composer token leak in CI logs (GHSA-f9f8-rm49-7jv2): patch Composer to 2.9.8 / 2.2.28 / 1.10.28 now
13 May 2026. Composer leaks GITHUB_TOKEN and GitHub App installation token values to CI logs once the new ghs_<id>_<base64url-JWT> format with hyphen kicks in (CVE-2026-45793 / GHSA-f9f8-rm49-7jv2, High, CVSS 7.5). Patches: Composer 2.9.8 (mainline), 2.2.28 (LTS), 1.10.28 (legacy). Leak window 12 May 22:00 UTC to 13 May 14:30 UTC; GitHub plans resumption on 18 May from 14:00 UTC.
TL;DR — the 90-second summary
Composer leaks tokens to CI logs. An update to 2.9.8 / 2.2.28 / 1.10.28 is mandatory. Anyone running GitHub Actions also has to check logs and potentially rotate tokens.
What happened?
Composer accepts tokens in Composer\IO\BaseIO::loadConfiguration() only if they match the regex ^[.A-Za-z0-9_]+$. GitHub's new format ghs_<numeric-id>_<base64url-JWT> contains hyphens. Validation fails, the rejected token is interpolated unredacted into the exception message and written to stderr via Symfony Console — where it lands in every CI log.
Who is affected?
Every Composer installation at versions 2.3.0–2.9.7, 2.0.0–2.2.27 and 1.0–1.10.27 running in a GitHub Actions environment with a GITHUB_TOKEN or GitHub App token in auth.json. Many workflows do this automatically (e.g. shivammathur/setup-php, already fixed).
What to do
Immediately: raise Composer to 2.9.8 (mainline) / 2.2.28 (LTS) / 1.10.28 (legacy). If you cannot update right away: disable affected workflows or GitHub Actions at organisation or repo level. Audit: grep job logs from the 14-hour leak corridor (May 12, 2026 ~22:00 UTC to May 13, 2026 ~14:30 UTC) per repository for contains invalid characters stack traces. GitHub plans to resume the token format rollout on May 18, 2026, from 14:00 UTC — anyone unpatched by then will run into the leak path again.
My customers
I don't use GitHub-hosted runners; my builds run in my own containers. These have been accelerated to 2.9.8 today. You don't have to do anything for my pipelines. If you operate GitHub Actions in your own repos, the steps above apply — I will support if needed.
Three sentences for decision-makers: The vulnerability is High severity (CVSS 7.5), the mitigation is trivial (one composer self-update), but the blast radius in the token worst case reaches 24 hours on self-hosted runners and potentially broader scope with App tokens. If you use GitHub Actions intensively, set up a log audit routine alongside the mitigation. If you don't use GitHub Actions, you still have to raise the Composer state in your own build container.
What is the problem?
Since 2021, Composer validates every configured GitHub OAuth token — including the GITHUB_TOKEN stored in auth.json — against a character-set regex. The code in src/Composer/IO/BaseIO.php (line 139 on main, line 143 on 2.8.x) reads:
// allowed chars for GH tokens are from
// github.blog/changelog/2021-03-04-authentication-token-format-updates/
// plus dots which were at some point used for GH app integration tokens
if (!Preg::isMatch('{^[.A-Za-z0-9_]+$}', $token)) {
throw new \UnexpectedValueException(
'Your github oauth token for '.$domain.' contains invalid characters: "'.$token.'"'
);
}
Three problems combine:
1. The token is interpolated verbatim into the exception message
When the regex fails, the complete token string is interpolated into the UnexpectedValueException message. Symfony Console then writes that message to stderr. Any environment that captures stderr (CI job logs, log shippers, monitoring, support tickets) now has the cleartext token.
2. The regex does not permit hyphens
GitHub's new structured format for App installation tokens has the shape ghs_<numeric-id>_<base64url-JWT>. Base64url encoding per RFC 4648 §5 uses - and _ as URL-safe replacements for + and /. Almost every base64url-encoded JWT signature contains at least one -. The 2021 Composer regex was chosen on the assumption that GitHub tokens use only [A-Za-z0-9_.]. That assumption no longer holds since the GitHub token format change.
3. GitHub Actions secret masker does not redact reliably
GitHub Actions' built-in secret masker only matches registered values as exact substrings. When the Composer exception is rendered by Symfony Console, the message may wrap, be embedded in In BaseIO.php line N: framing or interleaved with ANSI control sequences. The masker no longer finds the token substring and does not redact. The cleartext token reaches the log.
Trigger condition on the default path
Several widely-used GitHub Actions register the workflow GITHUB_TOKEN automatically into Composer's global auth.json — shivammathur/setup-php is the most prominent example (already fixed). Anyone using such an action and then calling composer install later in the workflow triggers the leak without further configuration.
Who is affected?
Three profiles:
Profile A — GitHub Actions users with Composer builds
Anyone running GitHub Actions for their own repositories and calling composer install or composer update in a workflow is the primary affected group. Especially TYPO3, Symfony, Laravel, Drupal and Magento repositories with CI pipelines. Urgency: high. Update immediately or disable workflows temporarily.
Profile B — Self-hosted runner operators
Anyone running GitHub Actions with self-hosted runners has an extended risk window: workflow GITHUB_TOKEN values can be refreshed for up to 24 hours (vs. up to 6 hours on GitHub-hosted runners). A leaked token in a log remains exploitable correspondingly longer.
Profile C — GitHub App users with Composer
If you combine actions/create-github-app-token or your own GitHub App installation tokens with Composer auth, you have an additional problem: these tokens have a default TTL of 1 hour, but can carry broader installation permissions than the workflow's own permissions: declaration — a leak grants potentially wider access than the job's stated rights.
Who is not directly affected?
Composer installations outside CI — developer machines, local builds, manual deployments. The leak lands here on the local console only, not in a persistent log aggregator. Still raise Composer, because validation also affects local tokens as soon as they take the new format.
Build pipelines without GitHub Actions — GitLab CI, Jenkins, Bitbucket, Drone, Buildkite, Forgejo Actions etc. If no GitHub App token sits in Composer's auth.json there is no leak vector. If you do use GitHub tokens there: same mitigation.
Packagist.org itself — it does not use a GitHub App and never runs Composer against App installation tokens.
Impact and token TTLs
The practical impact of a leak depends on the token type and runner environment. The Packagist blog differentiates this precisely — this is the operationally decisive table:
GitHub-hosted runner with workflow GITHUB_TOKEN
Maximum exposure window: 6 hours (job maximum execution time). The Composer exception usually terminates the job immediately, expiring the token at once. Practical worst case: 6 hours, realistically much shorter.
Self-hosted runner with workflow GITHUB_TOKEN
Max. job execution time is 5 days, but the GITHUB_TOKEN is an installation access token that can be refreshed for at most 24 hours per GitHub documentation. A self-hosted leak remains valid for up to 24 hours after issuance.
GitHub App installation tokens (e.g. via actions/create-github-app-token)
Default TTL: 1 hour. But permissions can be significantly broader than the workflow's own permissions: declaration. A leak therefore grants potentially more than the job is legally allowed.
What the tokens can do: typically contents:read, often contents:write (e.g. for Conductor-style actions). With App tokens, depending on configuration: actions:write, checks:write, issues:write, pull-requests:write. If you miss a leak and the token is still alive, the risks are: unwanted commits to default branches, tag/release manipulation, workflow triggers via API, or with broader scope code modifications across pull requests.
Plainly: the vulnerability is High severity, but real-world damage potential is capped in most cases by short token lifetimes on GitHub-hosted runners. On self-hosted runners and with App tokens the risk is substantially higher.
Mitigation and immediate actions
Quick start: raise Composer
# Composer phar self-update to a patched version
composer.phar self-update
# alternatively pin to a specific line explicitly
composer.phar self-update 2.9.8 # mainline
composer.phar self-update 2.2.28 # 2.2 LTS
composer.phar self-update 1.10.28 # legacy (rather upgrade to 2.x)
# verify Composer version
composer --version
If you get Composer from a distribution package manager (e.g. Debian/Ubuntu): wait for the distribution update or replace the binary directly via composer self-update or the getcomposer.org phar.
If you cannot update immediately: pause workflows
If the Composer update has to wait for organisational reasons, the official Packagist recommendation is clear: Disable any GitHub Actions workflow that runs Composer commands until you have updated Composer. Concretely in the GitHub UI:
At organisation level: Settings → Actions → General → Actions permissions → “Disable actions”.
At repository level: Settings → Actions → General → “Disable actions”.
Per workflow: comment out the workflow YAML or remove the on: triggers temporarily.
Update container/image builds
If you use your own build containers or PHP base images with a pinned Composer: rebuild the Composer binary in the image (e.g. COPY --from=composer:2.9.8) and re-publish the image tags. With TYPO3 hosting setups using FrankenPHP/PHP-FPM containers, Composer is typically active at build time — production containers need a rebuild.
auth.json hygiene
Check which tokens live in which auth.json locations:
Remove tokens that are no longer needed. For service accounts: check TTL, plan rotation.
Detection and log audit
Where to look
A Composer exception with token leak has a characteristic pattern. In a job log it looks like this:
In BaseIO.php line 139:
Your github oauth token for github.com contains invalid characters: "ghs_..."
Quick check per repository
# GitHub CLI: pull all job logs of the last 7 days and grep
gh run list --limit 100 --json databaseId --jq '.[].databaseId' | \
while read run_id; do
gh run view $run_id --log 2>/dev/null | \
grep -l 'contains invalid characters' && \
echo "LEAKED IN RUN $run_id"
done
If you find a hit: fetch the exact job log with gh run view <id> --log, extract the token values, revoke every leaked token immediately (GitHub App token via App settings, classical PATs via Personal Access Tokens) and delete the log file from repository storage if the token could still be alive (24h on self-hosted, 6h on GitHub-hosted, 1h on App default).
Proactive search for unauthorised activity
On a confirmed leak in a still-live window: review the GitHub audit log for token activity, at minimum these endpoints:
Repository push events since the leak time — unexpected commits, tags, releases.
Workflow run API triggers — were workflows started externally via API?
API audit log in GitHub Enterprise: unusual token user agents, foreign IP addresses.
Continuous monitoring
Even after patch and rotation: a simple CI step at the start of every workflow that checks the Composer version and fails the job if it is below 2.9.8 / 2.2.28 is an insurance policy against regression through stale container images.
Operator recommendation
Operational decision block
If you run GitHub Actions with Composer builds on GitHub-hosted runners — then
update Composer in all workflows to 2.9.8 (or 2.2.28 / 1.10.28) within the next hours. In parallel, scan the last 6–8 hours of job logs for the leak pattern. If you find hits: revoke the token, remove the log files.
If you run GitHub Actions on self-hosted runners — then
your exposure window is up to 24 hours. Update immediately, log-audit over the last 24–48 hours, cross-check all GITHUB_TOKEN activity in the audit log against the leak window.
If you use GitHub App installation tokens via Composer — then
the situation is most serious. Default TTL 1 hour, but broader permissions. Apply the update, rotate the GitHub App token, cross-check App audit log for API activity.
If you run your own build containers with pinned Composer version — then
image tag rebuild with Composer 2.9.8 (or 2.2.28), push image to registry, switch all deployment pipelines to the new tag version, mark old tags.
If you don't use GitHub Actions — then
the acute leak risk is reduced significantly. Still raise Composer to 2.9.8, because validation also affects any other build environment with future GitHub token formats.
What I deliberately do not do
No delayed update with “it's only an assumption”. The pre-conditions are not hypothetical — they are the standard workflow of many PHP CI pipelines.
No reliance on GitHub secret masking. The masker does not redact reliably for this vulnerability. Assumption: every token that appears in the Composer exception must be considered leaked.
No workaround via regex patch in Composer source code. The upstream update also redacts the exception message; a homegrown regex fix solves only one of three problems.
What I did
One important upfront point: I do not use GitHub-hosted Actions runners. My CI and build pipelines run on my own infrastructure — GitLab CI with GitLab Runner on my own hosts, plus dedicated build containers. GitHub App installation tokens are not used in my pipelines. The GHSA-f9f8-rm49-7jv2 leak condition is therefore not present in its primary form for my setup.
Even so: Composer is in there, so Composer gets patched
Composer is in practically every one of my PHP build containers — as a build tool to compose TYPO3, Symfony and site-package distributions. I run several container images:
moselwal/build-base with PHP + Composer as a base image for all site-package builds
moselwal/typo3-builder as a specialised build image for TYPO3 distributions
moselwal/frankenphp-runtime with FrankenPHP worker mode plus Composer for build steps in production pipelines
Today (13 May 2026) I raised all three image lines on accelerated maintenance to Composer 2.9.8. The new image tags are in the container registry, the build pipelines are switched to the new tags, smoke tests have run.
For customers on their own infrastructure
Anyone using my maintained build containers in their own CI pipelines (GitLab, Bitbucket, Drone) has the update through the image pull of the next build iteration. I recommend a single re-run per customer pipeline so the new container state takes effect in the next deployment roll-up.
For customers with their own GitHub Actions setup
If you additionally run your own GitHub repositories with GitHub Actions where Composer runs: you have to act yourself there. The steps in the Operational Decision Block above apply for you. I support if needed with log audit, token rotation and workflow hygiene.
Frequently asked questions about the Composer token leak
Conclusion
GHSA-f9f8-rm49-7jv2 is a classical defence-in-depth incident: not a code execution bug, not a memory corruption, but a validation regex from 2021 that no longer works with an evolved token format. The actual damage comes from the interaction of rejected-token-in-exception-message plus unreliable secret masking. The Composer team responded quickly — less than 11 hours between private report and published patch.
If you run GitHub Actions, patch today, review logs and rotate tokens on hit. If you don't run GitHub Actions, still raise Composer in all build containers — validation runs everywhere Composer handles tokens. And if you want to build a CI stack that is more robust against this class of vulnerability: fewer tokens in auth.json, narrower permissions on App tokens, shorter TTLs, log scrubbing as part of the CI pipeline rather than relying on platform maskers.
For my customers, the situation is relaxed because I don't run GitHub Actions as a build stack — even so, Composer has been raised to 2.9.8, because build cleanliness should not depend on assumptions about the build consumer.
Build pipelines that don't depend on platform maskers
I build TYPO3, Symfony and Laravel build stacks with clear token hygiene rules: narrow permissions, short TTLs, log scrubbing at pipeline level instead of relying on platform maskers. If you want to know where your CI pipeline has token-hygiene weaknesses, talk to me.
Programming since 2002 – self-taught, set up my own business with KO-Web in 2012. Over 100 projects, with a focus on security, performance, automation and quality. Today freelance: DevSecOps consulting, training and software development.