Composer 2.9.8 and 2.2.28 fix GitHub Actions token disclosure in error messages
Please immediately update Composer to version 2.9.8 or 2.2.28 (LTS) by running composer.phar self-update. The new releases fix a vulnerability where Composer leaks the full contents of GitHub Actions issued GITHUB_TOKENs or GitHub App installation tokens to the GitHub Actions logs. GitHub introduced a new format for these tokens including a - (hyphen). The new format is gradually being rolled out to repositories. The new format fails Composer’s validation, leading to an error message that exposes the full token contents to stderr. A CVE identifier will be assigned and added to this post once available.
The issue was first reported by kesselb on May 12, 2026 at 9:40 PM UTC as a regular public GitHub issue, then as a security issue on May 13, 2026 at 12:51 AM UTC. Details are available in the advisory GHSA-f9f8-rm49-7jv2 with a CVE ID still pending. The releases with fixes were published on May 13, 2026 at 7:28 AM UTC.
If you run Composer in GitHub Actions, you should treat this as urgent. We recommend disabling GitHub Actions on affected organizations or repositories until you have updated to Composer 2.9.8 or 2.2.28. See "Recommended immediate action" below.
Cause of the vulnerability
Composer validates configured GitHub tokens against an allowed character set in Composer\IO\BaseIO::loadConfiguration(). When validation fails, an UnexpectedValueException is thrown. Its message interpolates the rejected token verbatim:
Your github oauth token for github.com contains invalid characters: "<full token here>"Symfony Console then renders this message on stderr. In CI, anything written to stderr is captured in the job log and persisted wherever the CI provider stores logs.
Three factors combine to produce a leak:
- The rejected token is interpolated into the exception message. The exception bubbles up to Symfony Console's default error renderer, which writes it to stderr. Any environment that captures stderr (CI logs, log shippers, monitoring, support transcripts) now has the raw token.
- The validation regex did not permit -. GitHub's new structured format for GitHub App installation tokens uses a
ghs_<numeric-id>_<base64url-JWT>shape. Base64url encoding (RFC 4648 §5) uses-and_as the URL-safe replacements for+and/, so any base64url-encoded JWT signature is likely to contain at least one-. The regex was chosen in 2021 on the understanding that GitHub tokens use only[A-Za-z0-9_.]. - Detection / secret masking in GitHub Actions is unreliable. GitHub Actions' built-in secret masker matches registered values as exact substrings. When the exception message is rendered by Symfony Console it may wrap, frame in other text, or interleave with ANSI control sequences. So the masker does not redact, and the plaintext token reaches the log.
This is what makes the leak easy to trigger on the default path: Any workflow that configures a GitHub App installation token (including the workflow's auto-injected GITHUB_TOKEN) into Composer's auth and then runs any Composer command will hit it.
Several widely used GitHub Actions register the workflow GITHUB_TOKEN into Composer's global auth.json automatically: shivammathur/setup-php being a notable example (It has already been updated to use fixed Composer versions). Users do not need to opt into any unusual configuration; the leak occurs on the default code path whenever an affected token format is in use.
Recommended immediate action
If in any way possible, immediately update your Composer installation to a safe version. If you cannot update immediately, see workarounds section below.
Any workflow run that hits the validation path leaks its token to the job log. While GitHub Actions tokens expire at the end of the corresponding job, GitHub App Installation tokens do not. So, if you use these non-standard tokens for Composer authentication, consider reviewing logs for leaked tokens and deleting log contents if any of the tokens have not yet expired.
The lifetime of the leaked credential, and therefore the size of the exposure window, depends on the runner type and the credential's origin. The differences are significant and you should pay particular attention to self-hosted runners:
- GitHub-hosted runners. The workflow
GITHUB_TOKENexpires when the job finishes or after the job's maximum execution time of 6 hours, whichever comes first. The exposure window per leaked token is therefore at most 6 hours. Usually the error leaking the token will also fail the job and immediately expire the token. - Self-hosted runners. The maximum job execution time is 5 days, but the
GITHUB_TOKENis an installation access token that, per GitHub's documentation, can only be refreshed for up to 24 hours. A token leaked from a self-hosted runner job is therefore potentially valid for up to 24 hours after issuance. - Tokens created by
actions/create-github-app-tokenor another GitHub App. Installation access tokens issued via the GitHub Apps API are valid for up to 1 hour by default. The leaked token grants whatever installation permissions were requested, which can be considerably broader than the workflow's own declaration ofpermissions:.
Concretely: delete any token whose plaintext may have been written to a job log, treat the GITHUB_TOKEN on self-hosted runners as valid for up to 24 hours, and on GitHub-hosted runners as valid for up to 6 hours, confirm no unexpected activity occurred under that token.
Patches
The issue is fixed in Composer 2.9.8 (mainline) and Composer 2.2.28 (2.2 LTS) and Composer 1.10.28 (otherwise unsupported legacy release, rather upgrade to 2.x). The fix addresses the contributing factors:
- The exception message no longer includes the rejected token value. Diagnostic output identifies which credential failed validation without revealing its contents.
- The validation character set has been relaxed to accept
-, matching the character set of GitHub's current structured installation token format.
Redacting the exception message removes the underlying leak primitive for any future or third-party credential that fails validation.
Workarounds
If you cannot update Composer immediately:
- Disable any GitHub Actions workflow that runs Composer commands until you have updated Composer. You can also disable GitHub Actions entirely on your organization or potentially affected repositories in GitHub settings.
Impact on Packagist.org and Private Packagist
Packagist.org is unaffected: it does not use a GitHub App and therefore never runs Composer against GitHub App installation tokens.
Private Packagist uses Composer to access package data on GitHub using our GitHub App using the GitHub App's installation token, a rejected token could in principle have appeared in the package update log visible to anyone with access to the package. In practice no token was exposed: GitHub has not yet issued tokens in the new ghs_<id>_<JWT> format for our GitHub App, so the validation regex was never triggered. Private Packagist has already applied the Composer fix and we audited update logs to confirm no tokens appeared.
Like any GitHub Action, the Private Packagist Conductor GitHub Action will have leaked tokens for repositories already migrated to the new token format by GitHub, but they will have expired immediately. The workflow GITHUB_TOKEN for Conductor carries contents:write. If Composer is pinned to a specific version make sure to upgrade immediately, if you use shivammathur/setup-php the issue is already resolved.
Private Packagist Self-Hosted neither uses the GitHub App for repository access nor ships with Conductor and is therefore unaffected. A new release including the patched Composer version will still be published shortly.