Kai Ole Hartwig — Blog
16 min read
High
By

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.

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 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.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 I deliberately do not do

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:

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.

Talk to us

Author of this post

[Translate to English:] Foto von Kai Ole Hartwig.

Kai Ole Hartwig

Freelance DevSecOps consultant · OnlyOle Consulting

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.