One Maintainer's Unmerged Pull Request Exposed a CI Token Leak That Was Active for Eight Months
A lone maintainer was debugging a flaky CI job when they noticed something odd in the build logs: an environment variable named PROD_DEPLOY_TOKEN was being echoed in plaintext. The token, scoped to write access on a popular npm library's repository, had been exposed for eight months. The pull request that introduced the leak was never merged—it sat unreviewed, abandoned by a contributor who had moved on. By the time the maintainer spotted it, the token had rotated twice, but the leak window spanned over 240 days. This is not a rare incident. It is a symptom of how CI pipelines are built, reviewed, and ignored.
The Pull Request That Was Never Merged
The discovery happened during a routine CI debugging session. The maintainer, who prefers to remain anonymous due to past harassment, was tracing a failed job in a GitHub Actions workflow. The logs showed a step that printed all environment variables for debugging—a common practice in early-stage development. Among the variables was PROD_DEPLOY_TOKEN, a credential used to push releases to a package registry. The token was visible in plaintext, accessible to anyone who could view the log output.
The maintainer traced the variable back to a pull request opened eight months earlier by an occasional contributor. The PR aimed to add a new CI job for automated deployment. The contributor had defined the token as a repository secret but also echoed it in a debug step, presumably for testing. The PR was never merged—it was left open, with no reviewer comments, no merges, no activity. Yet the token exposure persisted because the CI configuration file that contained the echo statement was still present in a branch that could be triggered manually.
The token itself was a personal access token with the repo scope, granting full control over the repository including the ability to push commits, create releases, and modify workflows. An attacker who obtained it could have injected malicious code into the package, affecting thousands of downstream users. The maintainer revoked the token immediately, but the incident raised uncomfortable questions: How many similar tokens are leaking right now, unnoticed?
The PR's abandonment is a familiar story in open source. Contributors drift away, maintainers are overwhelmed, and low-priority changes languish. The PR had no labels, no assignee, and no automated checks beyond basic linting. It was simply forgotten.
How a Leak Survives Eight Months Undetected
The leak persisted because no automated secret scanning was applied to CI logs. GitHub's secret scanning, as of early 2025, covers committed code but not ephemeral CI output. The logs were stored for 90 days by default, but the token was printed in every run of that branch, so it appeared repeatedly. No alert was triggered because the pattern—a token echoed in a debug step—did not match known formats like ghp_* or sk-*.
Human review was skipped because the PR was never assigned. The project had a policy of requiring at least one reviewer for changes to the CI configuration, but that policy was not enforced. The PR sat in a queue of hundreds of open issues and pull requests. The maintainer who eventually discovered the leak was not the same person who had reviewed previous CI changes—they were simply the one who ran the job that failed.
The token was rotated quarterly, but the leak predated the rotation cycle. An attacker who had scraped the logs at any point in the eight-month window could have used the token until the next rotation. Even after rotation, the new token could have been exposed in the same way if the debug step was still active. The project's rotation policy was manual and did not include checking for exposed secrets in logs.
The compromise window—the time during which an attacker could have exfiltrated the token and used it—was at least 240 days. During that period, the repository had 12 releases, each of which could have been tampered with. No evidence of malicious activity was found, but the maintainer acknowledged that an attacker could have silently cloned the repository, extracted secrets, and used them without leaving obvious traces.
The Economics of Ignoring Supply-Chain Security
Why do such leaks persist? The answer is partly economic. A dedicated security engineer costs roughly $200,000 per year in total compensation. Many startups and mid-sized companies skip this line item, relying on existing engineers to handle security as a side task. Open-source projects have even less budget—they depend on volunteer maintainers and occasional grants.
The average cost of a software supply-chain breach, according to a 2024 industry report, is around $2.4 million when factoring in remediation, legal fees, and reputational damage. That is roughly 12 times the annual salary of a security engineer. Yet the upfront cost of hiring such an engineer is visible on a balance sheet, while the risk of a breach is deferred and uncertain. Venture capital pressure to ship features quickly compounds the problem—CI pipeline security is rarely a priority in sprint planning.
Open-source maintainers bear the monitoring burden disproportionately. A 2025 survey by the Linux Foundation found that 60% of maintainers spend less than five hours per week on security-related tasks. They are expected to review code, manage secrets, and respond to vulnerability reports, all without compensation. The token leak described here was discovered only because a maintainer happened to look at the logs—not because a systematic process caught it.
The economics also explain why commercial secret scanners are not universal. A GitHub Advanced Security license costs roughly $49 per contributor per month. For a project with 50 active contributors, that is nearly $30,000 per year—a sum that many open-source foundations cannot justify. Free tiers exist but often lack coverage for CI logs.
Real-World Fallout: What a CI Token Unlocks
A CI token with write access is a skeleton key. In this case, the token could push commits to the main branch, create releases, and modify GitHub Actions workflows. An attacker could have added a malicious step to the CI pipeline that exfiltrated other secrets, such as cloud storage credentials or API keys for downstream services.
The token also allowed access to the npm registry where the library was published. An attacker could have pushed a new version containing a backdoor, a pattern seen in the 2024 PyPI token compromise, where a leaked token allowed an attacker to publish malicious packages under a legitimate account. The same technique could have been used here, affecting any application that depended on the library.
Dependency confusion attacks are another vector. If the token granted access to a private registry, an attacker could upload a package with the same name as a public dependency, tricking build systems into using the malicious version. The token's scope was broad—it was created for convenience, not least privilege. This is a common anti-pattern: tokens are often over-scoped to avoid the hassle of managing multiple credentials.
Downstream repositories that forked the project or used it as a dependency were also at risk. A malicious commit pushed to the main branch would propagate to all forks that synced upstream. The maintainer estimated that the library was used by roughly 500 other open-source projects, based on dependency graph data. A single compromised token could poison thousands of builds.
Why Current Tools Miss These Leaks
Existing secret scanners are designed for committed code. They scan git history for patterns like -----BEGIN PRIVATE KEY----- or ghp_ tokens, but they rarely inspect CI logs. GitHub Actions logs are stored as plain text and are not indexed for secret patterns by default. A token printed in a log is effectively invisible to automated tools unless a custom script is written to scan them.
Ephemeral CI logs are rarely audited because they are considered transient. Most teams set a retention period of 30 to 90 days and then delete them. But an attacker who monitors logs in real time can capture secrets before deletion. The maintainer in this case found the leak only because they were manually inspecting a failed job—a practice that is not scalable.
There is no standard for token expiration in CI environment variables. Many projects set tokens to never expire, or they use long-lived tokens rotated annually. Short-lived tokens—valid for hours or minutes—are technically feasible but require infrastructure to issue and refresh them. Most CI platforms do not natively support this; they rely on static secrets stored in repository settings.
Open-source projects lack the budget for commercial scanners that cover CI logs. Tools like GitGuardian or TruffleHog offer CI log scanning in their enterprise tiers, but the free versions focus on git history. The result is a blind spot that attackers are increasingly exploiting. A 2025 analysis by a security research group found that 12% of sampled open-source projects had at least one secret exposed in CI logs over a six-month period.
Fixing the Pipeline: Practical Steps for Teams
The first fix is to enforce mandatory pull request review for CI configuration changes. This is a low-cost policy change: configure branch protection rules to require at least one approval for any file under .github/ or ci/. The maintainer's project now does this, but it took a near-miss to implement it.
Short-lived tokens are the second line of defense. Instead of storing a static token in repository secrets, teams can use OpenID Connect (OIDC) to issue tokens that are valid only for the duration of a single job. GitHub Actions supports OIDC for cloud providers like AWS and Azure, and the setup takes a few hours. The token in the leaked PR was a static token; if OIDC had been used, the exposure window would have been minutes, not months.
Automated CI log auditing is another step. Teams can write a simple script that runs after each job and scans the log for patterns matching known secret formats. Open-source tools like trufflehog can be integrated as a post-job step. The script can fail the job if a secret is detected, preventing the log from being stored. This approach is not foolproof—attackers can obfuscate secrets—but it catches the low-hanging fruit.
Token rotation policies should be tied to merges to the main branch, not to a calendar. Every time a change is merged, the token should be rotated. This limits the damage from any single exposure. The practice is common in high-security environments but rare in open source due to the operational overhead of updating secrets across multiple services.
Adopting the OpenSSF Scorecard is a broader step. The Scorecard checks for practices like branch protection, token permissions, and dependency updates. It is free and runs as a GitHub Action. The maintainer's project now runs it weekly, and it flags any workflow that uses a token without OIDC. The Scorecard is not a silver bullet, but it raises the baseline.
A Second Case Study: The Python Package Registry Leak
To illustrate how pervasive these issues are, consider a similar incident from early 2025 involving a popular Python data processing library. In that case, a contributor accidentally committed a file containing a PyPI API token to the repository's .github/workflows directory. The token was not flagged by GitHub's secret scanning because the file extension was .yaml, which is not scanned by default. The token was exposed for six months before a security researcher discovered it during a routine audit of public repositories. The researcher reported the leak privately, and the maintainers revoked the token within hours. However, during that six-month window, the token could have been used to publish malicious releases to PyPI, affecting any project that depended on the library. The library had over 10 million weekly downloads on PyPI.
This second case shares the same root causes: lack of automated scanning for non-standard file types, over-scoped tokens with write access to the package registry, and insufficient oversight of CI configuration changes. The maintainers had not enforced branch protection rules for the .github directory, and the token was a long-lived personal access token with the write:packages scope. After the incident, they implemented OIDC-based authentication for PyPI uploads and added a pre-commit hook to detect secrets in YAML files. These fixes were straightforward but required dedicated effort—effort that many projects cannot spare.
The Unmerged PR as a Warning
That maintainer's unmerged PR is now a case study in conference talks and blog posts. It has been cited in at least two security advisories as an example of how supply-chain vulnerabilities originate not from sophisticated attacks but from mundane neglect. The PR itself was eventually closed, but the underlying issues—unreviewed changes, over-scoped tokens, absent log scanning—remain widespread.
The industry is slowly adopting pre-merge CI scanning. GitHub introduced secret scanning for Actions logs in beta in early 2026, but adoption is low. Most teams do not enable it because it requires a GitHub Advanced Security license and generates noise. The balance between false positives and true leaks is still being tuned.
But most projects still rely on unpaid vigilance. Open-source maintainers are expected to catch leaks in their spare time, without training or tooling. The token leak described here was a lucky catch—a maintainer noticed something odd. The next eight-month leak is almost certainly already active, sitting in a forgotten PR or a stale branch, waiting for someone to look.
So what can you do, starting tomorrow? First, audit your own CI logs. Check the last 90 days of logs for any environment variable that appears in plaintext. Second, enforce branch protection for CI configuration files. Third, adopt OIDC for any token that grants write access to a package registry or cloud provider. Fourth, run a tool like the OpenSSF Scorecard on your repository. These steps are not expensive, and they do not require a dedicated security team. They require only the discipline to treat CI configuration as production code—and the humility to assume a leak is already there.