# Write-scoped CI token exposure

Does untrusted fork/PR code, external fetch, or a stalled job reach a write-scoped CI token?

_Area: Software supply chain and CI/CD. Seen in 2 disclosed reports; 2 confirmed bounties: $1,500 and $25,000; latest 2021-04-02. Updated 2026-11-01._

## How it works
CI systems hand each job a credential so it can act on the repo: a `GITHUB_TOKEN`, a CI job token, an account-wide email token, or a developer's bearer token. The broken assumption is that the job's trust level matches the token's power. When untrusted code (a fork PR, an attacker-controlled `$ref` URL, a stalled runner) runs in the same privileged context, it inherits a token that can write, push, or act account-wide. The token's scope is set by the platform's default, not by who triggered the job.

## Where it shows up
- Workflows triggered by `pull_request` from forks that still receive a write-scoped base-repo token [E1](https://bugbountylibrary.com/r/blog%3Ablog-tyage-net-posts-2021-04-02-improper-access-control-github-workflow.md)
- "Private" addresses or share links with an embedded token presented as narrower than it is [E2](https://bugbountylibrary.com/r/blog%3Aaikido-dev-blog-gitlab-email-push-to-main.md)
- Credentials attached unconditionally to outbound fetches of user-supplied URLs (`$ref`, imports, remote includes) [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-h754-fxp7-88wx.md)
- Job tokens whose lifetime is tied to job completion, not to the user's current account state [E4](https://bugbountylibrary.com/r/h1%3A497047.md)
- Long-lived account-wide tokens where a project-scoped one is expected [E2](https://bugbountylibrary.com/r/blog%3Aaikido-dev-blog-gitlab-email-push-to-main.md)

## How to review
Identify every credential a job or tool can reach, then ask: can untrusted input reach this same context?
- For fork/PR workflows: confirm the token is read-only or absent for untrusted triggers; the invariant is that untrusted code never runs with a token scoped to the base repo's write access [E1](https://bugbountylibrary.com/r/blog%3Ablog-tyage-net-posts-2021-04-02-improper-access-control-github-workflow.md).
- For embedded tokens: extract the token and test it against other resources and alternate suffixes; scope in the UI must match scope in practice [E2](https://bugbountylibrary.com/r/blog%3Aaikido-dev-blog-gitlab-email-push-to-main.md).
- For outbound fetches: trace where user-supplied credentials are attached; the invariant is same-origin/allowlist checking before any credential leaves for an attacker-controlled host [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-h754-fxp7-88wx.md).
- For job tokens: verify the token stops working once the triggering user is blocked or the job is abandoned; expiry must not depend on the runner reporting completion [E4](https://bugbountylibrary.com/r/h1%3A497047.md).

## How finders spotted it
- Noticed shared compute for fork PR jobs and hypothesized the token might be shared too, then tested with a PR [E1](https://bugbountylibrary.com/r/blog%3Ablog-tyage-net-posts-2021-04-02-improper-access-control-github-workflow.md).
- Saw the UI framed an address as project-scoped, then found the same token across five project addresses including private ones [E2](https://bugbountylibrary.com/r/blog%3Aaikido-dev-blog-gitlab-email-push-to-main.md).
- Code review of the header-builder found the headers object unconditional, then traced it to every external `$ref` fetch [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-h754-fxp7-88wx.md).
- Noticed blocked users lose UI/API/git but CI jobs still run, then tested whether suppressing runner status updates extends the token [E4](https://bugbountylibrary.com/r/h1%3A497047.md).

## Variants seen
- Fork PR inheriting base-repo write token [E1](https://bugbountylibrary.com/r/blog%3Ablog-tyage-net-posts-2021-04-02-improper-access-control-github-workflow.md)
- Account-wide token hidden in a "project-scoped" email address [E2](https://bugbountylibrary.com/r/blog%3Aaikido-dev-blog-gitlab-email-push-to-main.md)
- Developer bearer token leaked to attacker `$ref` hosts [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-h754-fxp7-88wx.md)
- Job token outliving account block via stalled runner [E4](https://bugbountylibrary.com/r/h1%3A497047.md)

## Impact and severity
Ranged from force-push to main [E1](https://bugbountylibrary.com/r/blog%3Ablog-tyage-net-posts-2021-04-02-improper-access-control-github-workflow.md) to account-wide code push and CI/CD execution bypassing IP allowlists [E2](https://bugbountylibrary.com/r/blog%3Aaikido-dev-blog-gitlab-email-push-to-main.md), token exfiltration whose blast radius follows the stolen token's scope [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-h754-fxp7-88wx.md), and access to projects created after a block [E4](https://bugbountylibrary.com/r/h1%3A497047.md). Severity rises with token scope and reach; chaining (email to merge-request to CI/CD) and bypassing network controls raised impact [E2](https://bugbountylibrary.com/r/blog%3Aaikido-dev-blog-gitlab-email-push-to-main.md).

## Evidence
- E1: https://bugbountylibrary.com/r/blog%3Ablog-tyage-net-posts-2021-04-02-improper-access-control-github-workflow.md
- E2: https://bugbountylibrary.com/r/blog%3Aaikido-dev-blog-gitlab-email-push-to-main.md
- E3: https://bugbountylibrary.com/r/ghsa%3AGHSA-h754-fxp7-88wx.md
- E4: https://bugbountylibrary.com/r/h1%3A497047.md
