High

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.

Handgesetzter Drucker-Setzkasten aus Walnuss mit Metalllettern, ein separates Hyphen-Type auf einem Kraftpaper-Etikett mit oxblutfarbenem REJECTED-Stempelabdruck. Daneben eine messingfarbene Karteikartenkassette mit halb herausgezogener Karte, deren Token-String von einer Stencil-Schablone teilweise verdeckt ist. Im Hintergrund die helle Glasfront eines modernen Moselhauses mit sonnigem Weinberg-Hang.

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 the last 24 hours of job logs per repository for contains invalid characters stack 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.jsonshivammathur/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?

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:

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:

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

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:

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.

Talk to us