Composer token leak in CI logs (GHSA-f9f8-rm49-7jv2): patch Composer to 2.9.8 / 2.2.28 / 1.10.28 now
On 13 May 2026 the Composer team published security advisory GHSA-f9f8-rm49-7jv2. Composer leaks GITHUB_TOKEN values and GitHub App installation tokens to CI logs once the tokens carry the new ghs_<id>_<base64url-JWT> format with a hyphen. Severity: High, CVSS 7.5, CVE pending. Patches: Composer 2.9.8 (mainline), 2.2.28 (2.2 LTS), 1.10.28 (legacy).
Note for Moselwal customers: we do not use GitHub-hosted runners. Our build containers run on our own infrastructure — Composer is in them nevertheless. We have already accelerated the update of all build environments and containers to Composer 2.9.8 today. If you operate GitHub Actions for your own repositories yourself, you still have to act — this post lists the steps.
What has changed? GitHub is gradually rolling out a new structured token format that contains hyphens. Composer's token validation regex ^[.A-Za-z0-9_]+$ does not accept hyphen and interpolates the rejected token unredacted into the exception message — which Symfony Console then writes to stderr. The GitHub secret masker is not reliable there. Who is affected? Anyone running Composer in GitHub Actions where a GitHub App installation token or the auto-injected GITHUB_TOKEN ends up in Composer's auth.json. What should you do today? Update to 2.9.8 / 2.2.28, review job logs, rotate tokens if needed, delete logs if needed.

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 formatghs_<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 the last 24 hours of job logs per repository for
contains invalid charactersstack traces.- Moselwal customers
We don't use GitHub-hosted runners; our builds run in our own containers. These have been accelerated to 2.9.8 today. You don't have to do anything for our pipelines. If you operate GitHub Actions in your own repos, the steps above apply — we 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 installorcomposer updatein 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_TOKENvalues 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-tokenor 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 ownpermissions: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.jsonthere 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:
# Global auth.json
cat ~/.composer/auth.json
cat ~/.config/composer/auth.json
# Project-local
cat ./auth.json
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 we 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 we actually did at Moselwal
One important upfront point: we do not use GitHub-hosted Actions runners. Our CI and build pipelines run on our own infrastructure — GitLab CI with GitLab Runner on our own hosts, plus dedicated build containers. GitHub App installation tokens are not used in our pipelines. The GHSA-f9f8-rm49-7jv2 leak condition is therefore not present in its primary form for our setup.
Even so: Composer is in there, so Composer gets patched
Composer is in practically every one of our PHP build containers — as a build tool to compose TYPO3, Symfony and site-package distributions. We 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) we 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 our maintained build containers in their own CI pipelines (GitLab, Bitbucket, Drone) has the update through the image pull of the next build iteration. We 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. We support if needed with log audit, token rotation and workflow hygiene.
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 our customers, the situation is relaxed because we 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.
Frequently asked questions about the Composer token leak
Why did it take 5 years for this regex bug to surface?+
The 2021 Composer validation regex was correct against the GitHub token format of that era. Only GitHub's transition to ghs_<id>_<base64url-JWT> with hyphens broke the regex assumption. This is a typical case of protocol drift: correct validation ages along with its assumptions about the validation input. The lesson for your own build pipelines: re-validate token format checks periodically against the current specification, don't treat them as stable for 5 years.
We are a Moselwal customer — do we have to do anything?+
For the build pipelines we operate: no. We don't use GitHub-hosted runners, and our build containers were raised to Composer 2.9.8 today on accelerated maintenance. If you additionally run your own GitHub Actions setups for your repositories where Composer runs, the update recommendation applies to you separately — we support if needed with log audit and workflow hygiene.
Doesn't the GitHub secret masker normally catch this?+
Not reliably in this case. The masker does exact substring matches. When Symfony Console renders the exception, the token string can be interleaved with line breaks, In BaseIO.php line N: framing or ANSI codes. The masker doesn't find the match and doesn't redact. That is exactly one of the three contributing factors to the leak.
How do I find out whether a token leaked in my setup?+
Search your GitHub Actions job logs for the pattern contains invalid characters or BaseIO.php line. With GitHub CLI: gh run list --limit 100 --json databaseId --jq '.[].databaseId' | xargs -I {} gh run view {} --log | grep -l 'contains invalid characters'. On hits: inspect the job log individually and extract the token value.
What is the concrete damage if a token leaks?+
The leaked token grants the permissions it was issued with. For workflow GITHUB_TOKEN: typically contents:read, often contents:write. This enables unauthorised commits, tag/release manipulation, workflow triggers. For App tokens the scope can be substantially broader (issues, PRs, checks, actions write). The token lifetime caps the window: 6h GitHub-hosted, 24h self-hosted, 1h App default.
Do we really have to update Composer immediately?+
If you run Composer in GitHub Actions: yes, immediately. The vulnerability is High severity (CVSS 7.5), the mitigation is trivial (composer.phar self-update), and the new GitHub token format is being rolled out gradually. It is not whether but when your repositories will receive the new format.
Build pipelines that don't depend on platform maskers
We 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 us.
![[Translate to English:] TYPO3 14.3.1 und 13.4.29 — Maintenance-Releases im Betreiberüberblick [Translate to English:] Mattschwarze Server-Edge-Box auf Walnuss-Werkbank mit aufgeklapptem Laptop, der einen TYPO3-Backend-Pagetree zeigt; daneben zwei Kraft-Paper-Module-Cartridges mit Mono-Labels 14.3.1 und 13.4.29, im Hintergrund Mosel-Schiefer-Weinberg-Terrassen im Morgennebel.](/fileadmin/_processed_/c/6/csm_8594429e301ce9c276f63542f71775511bd1e0e5f4402532b644325d439c338f_9a3584a49e.jpg)