
Finding and Rotating Leaked GitHub Tokens in CI Logs: The Composer Bug as a Case Study
What the Composer bug changed in CI output
Composer is usually the boring part of CI: install dependencies, print status, exit. This bug changed that assumption and turned a routine dependency step into a secret exposure path. GitHub tokens that should have stayed inside the auth layer could end up in build logs.
That matters because CI logs are often treated like low-risk storage. Read access is broad, retention is long, and logs get shipped to external systems for indexing. Once a token lands there, the leak stops being hypothetical.
Why leaked GitHub tokens in logs are an incident
A token in a log is not just “visible output.” It is a credential with whatever scope you granted it. If it can read private repos, access packages, or call GitHub APIs, an attacker who finds the log can reuse it before you notice.
Where the exposure happens in a pipeline
The risky spots are usually simple:
- dependency install steps
- verbose Composer output
- job summaries that capture stdout and stderr
- build artifacts that bundle terminal output
- third-party log aggregation systems
I usually treat every install step as secret-bearing. It often reads from environment variables, auth files, or helper commands that were never meant to be human-readable.
Why redaction can fail or be bypassed
Redaction helps, but it is not a guarantee. It can miss secrets when:
- the token is transformed before printing
- the value is split across lines
- the sanitizer only knows exact strings
- a wrapper command prints the token in a different format
- the secret is masked in one layer but not in another exported artifact
A bug in the tool itself is worse than a naive echo, because teams trust the tool not to expose secrets at all.
Reproducing the risk safely
You do not need a real token to test the workflow. Use a dummy value and confirm whether it appears in output or artifacts.
Minimal Composer workflow example
name: test-composer
on: [push]
jobs:
build:
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: "ghp_example_dummy_token_1234567890"
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: composer install --no-interaction --no-progress -vvv
The point is not to exploit Composer. It is to see whether sensitive values can bleed into stdout when the job runs with verbose flags or auth configuration.
What to look for in logs and artifacts
Check for:
- the raw token value
- partial token fragments
- URLs with embedded credentials
- auth headers or helper command output
- verbose traces from dependency resolution
If your CI stores raw logs, search both the terminal view and the exported archive. I have seen teams check the job page and miss the copied artifact entirely.
Auditing for exposed tokens after the patch
Search CI logs, artifacts, and job summaries
Start with the obvious places:
- recent successful and failed pipeline runs
- all logs from jobs that run Composer
- downloadable artifacts
- job summaries and annotations
- external log storage
Use a pattern that matches your token format, but do not rely on a single exact string. Look for prefixes like ghp_, github_pat_, or any internal token naming convention your team uses.
Check token scope, age, and usage history
Once you find a candidate leak, ask three questions:
| Check | Why it matters |
|---|---|
| Scope | Determines what an attacker could access |
| Age | Older tokens may still be valid across many jobs |
| Usage history | Shows whether the token was already used outside normal CI |
If the token had broad repo or package access, treat it as an incident even if you have not seen abuse yet.
Rotating and invalidating the right credentials
Revoke GitHub tokens and reissue only what is needed
Do not just regenerate everything with the same scope. Revoke the exposed token first, then reissue the minimum credential needed for the specific job.
If the pipeline only needs read access to a single private repository or package feed, do not give it org-wide write access. That is how a log leak becomes a larger compromise.
Update secrets in CI and local developer machines
Rotation is incomplete if the old token still exists in:
- repository secrets
- org-level CI variables
- local
.envfiles - developer shell history
- package manager auth config
- secret managers mirrored into other systems
I like to make a short inventory after rotation so one credential does not survive in three places.
Hardening Composer and CI against repeat leaks
Disable noisy debug output in non-interactive jobs
Use the least verbose mode that still gives useful failure signals. In practice, that means avoiding debug flags unless you are actively diagnosing a broken install.
For PHP pipelines, it helps to make a rule: verbose dependency output is for break-glass debugging, not default CI.
Treat dependency installs as secret-bearing steps
Composer often sits in the same trust boundary as deploy scripts. It reads auth, touches package registries, and runs in environments that already hold secrets. That means you should:
- isolate install jobs from unrelated logs
- mask environment variables at the CI platform level
- restrict who can read build output
- keep secret material out of generated artifacts
- review dependency tooling after every security patch
Practical checklist for teams on PHP pipelines
- Patch Composer everywhere you run it.
- Search recent logs for GitHub token patterns.
- Check artifacts, summaries, and external log stores.
- Revoke exposed tokens before reissuing replacements.
- Reduce token scope for each pipeline job.
- Remove stale credentials from local and CI environments.
- Keep dependency installs quiet unless you are debugging.
Conclusion
The important part of this incident is not that Composer printed something embarrassing. It is that a normal install step became a secret leakage channel in systems that already collect too much output by default.
If your pipeline uses Composer and GitHub auth, assume the logs are sensitive until you prove otherwise. Patch first, audit second, rotate third. That order is boring, but it is the one that keeps a CI mistake from turning into a GitHub compromise.


