This is the second post in a pair (the first covered the April 2022 GitHub/npm campaign as a whole): the Heroku-specific anatomy. On 26 April 2022, GitHub published its first postmortem confirming that the OAuth tokens abused against npm had been stolen from Heroku’s infrastructure — but the deeper story was what the Heroku compromise revealed about how platform integrations store other people’s credentials, and how one vendor’s environment variables became the skeleton key to an ecosystem. The attacker, having gained access to Heroku’s internal infrastructure (Heroku’s own disclosure attributed the initial access to a compromised token in a cached CI environment), was able to read the OAuth integration tokens GitHub had issued for the Heroku Dashboard and Heroku CI apps — tokens representing thousands of customer organisations’ authorisations, held in Heroku’s runtime for the integration to function. Those tokens were then used against GitHub’s API to enumerate and access npm’s private orgs, culminating in the registry-data and manifest-token exposures already covered. Heroku’s role made the case study unique: not the breached company’s own data at risk, but the delegated credentials of a third party (GitHub) and downstream parties (every org authorising the apps) — a nesting-doll of trust that forced every SaaS platform with OAuth integrations to re-examine where vendor-tokens live, what scopes they carry, and what an attacker sees when they land in the environment next door. The post-incident changes — GitHub’s vaulted integration-token design, Heroku’s infrastructure hardening, and the ecosystem’s adoption of token-exchange patterns (short-lived, audience-scoped tokens) — are the direct descendants of this single stored-credential architecture decision.
The Heroku half of the April 2022 GitHub/npm campaign: attacker compromised Heroku’s infrastructure (initial access assessed as a stolen token in a cached CI environment), gained read access to the stored OAuth tokens of GitHub’s first-party Heroku integration apps (Heroku Dashboard + Heroku CI apps) — tokens representing org-authorisations across thousands of GitHub orgs — then used them against GitHub’s API to reach npm’s orgs (the registry side covered in the campaign overview). Heroku’s April–May disclosures + GitHub’s postmortems established: no Heroku customer application data was compromised (app source, config vars of customer apps etc. untouched — the breach was in Heroku’s platform infrastructure holding the GitHub-side integration credentials), and the incident’s root lesson is architectural: OAuth integration tokens held by vendors are third-party production secrets — they must be stored token-vaulted, scoped minimally, rotated routinely, and monitored for anomalous use, because the vendor’s compromise is the customer-adjacent blast radius. Post-incident: GitHub moved npm/integration tokens to vaulted storage with rotation; Heroku hardened CI caching; the industry adopted (accelerated) token-exchange standards (RFC 8693, short-lived audience-bound tokens) so a stolen integration token is a dead-end rather than a skeleton key.
What happened
Heroku (Salesforce-owned platform-as-a-service) maintained GitHub integration apps so customers could deploy from GitHub repos and run CI against them. For the integration to work without per-use re-consent, Heroku’s platform stored OAuth tokens — issued by GitHub, scoped to the app’s requested permissions (repo read, org membership, statuses) — long-term in Heroku’s infrastructure. That’s standard OAuth app architecture: the vendor holds the token you granted.
In late March 2022, an attacker compromised Heroku’s platform infrastructure (assessed entry: a token stolen from an unprotected cached-CI environment; exact patient-zero details remained sparse in Heroku’s disclosures), landing in an environment where those stored integration tokens were readable. The attacker exfiltrated the tokens for the GitHub-owned integration apps. Because those apps were first-party-trusted (GitHub’s own Heroku integration), the stolen tokens carried whatever authority customer orgs had granted the app — and could be exercised directly against GitHub’s API without touching Heroku again. The npm pivot (April 12 mirror-environment access, manifest-token downloads, package tampering) followed, as covered in the campaign post.
Heroku’s disclosures (April 15 onward, coordinated with GitHub’s incident reports) were candid about the boundary: customer application code and customer app config vars were not exposed; the compromise lived in the platform layer where the GitHub-side credentials resided. The remediation combined Salesforce/Heroku infrastructure hardening, credential rotation, and (on GitHub’s side) the systemic fix: stopping the storage pattern that made a vendor breach a graph-wide event.
Why vendor-held tokens are a skeleton key
The stored-token trust pattern (and failure):
NORMAL OPERATION
org admin authorises "Heroku
Dashboard" GitHub App
-> GitHub issues OAuth token(s)
-> Heroku platform STORES token
long-term (integration needs
it to function without
re-consent)
token's authority = union of
every org's granted scopes
ATTACKER POSITION (post-compromise)
read access to Heroku platform
env holding tokens
= holds GitHub-API credentials
valid against thousands of
orgs
from GitHub's view: requests
are "the app" acting within
scope — because they are
WHY FIRST-PARTY APPS MAGNIFY IT
GitHub's own Heroku app was
pre-trusted, broadly granted
(org-wide default scopes era)
no per-org anomaly baseline
existed for app-identity
behaviour
POST-INCIDENT ARCHITECTURE
token vaulting: encrypted at
rest, narrow decrypted-in-use
window, rotation schedules
token exchange (RFC 8693):
long-term credential stays
in vault; short-lived
audience-scoped tokens are
minted per operation
anomaly detection keyed on
app identity x grant x source
(vs. just user logins)
NET EFFECT
vendor compromise -> dead-end
expired token, instead of
graph-wide standing access
Impact and numbers
| Metric | Value |
|---|---|
| Vendor compromised | Heroku (platform infrastructure), late March 2022 |
| Assessed entry | Stolen token in a cached CI environment |
| Credential class stolen | OAuth tokens for GitHub’s first-party Heroku Dashboard/CI integration apps |
| Downstream effect | npm org access → ~109K manifest-tokens downloaded; 3 packages tampered (see campaign post) |
| Heroku customer data | App code/config vars NOT exposed; platform-layer credential store was the blast zone |
| Systemic fixes | GitHub integration-token vaulting + rotation; Heroku platform hardening; RFC 8693 token-exchange adoption accelerated |
| Disclosure record | GitHub incident reports 2022-04-15+; Heroku/Salesforce advisories April–May 2022 |
Timeline
| Date | Event |
|---|---|
| 2022-03 late | Attacker enters Heroku platform infra via compromised cached-CI token; extracts GitHub integration-app OAuth tokens |
| 2022-04-12 | Tokens exercised against GitHub orgs → npm mirror access → manifest-token/metadata downloads |
| 2022-04-15 | GitHub discloses campaign; token revocations begin; Heroku advisory follows (coordination) |
| 2022-04→06 | Postmortems enumerate exposure; Heroku hardening (CI caching, credential storage); npm credential rotations complete |
| 2022→2024 | GitHub ships vaulted integration-token design; ecosystem adoption of token-exchange patterns spreads (short-lived, audience-scoped) |
Why it still matters in 2026
Because the stored-token problem didn’t die with the incident’s news cycle — it became the template for understanding every subsequent vendor-side credential compromise. The 2022 Heroku case established the grammar: a SaaS platform holds delegated credentials whose authority spans its entire customer graph; a compromise there is not “the vendor’s breach” but a graph-wide event whose blast radius was decided months or years earlier at authorisation-time scope-grants. The 2024–2025 Snowflake-customer campaign (compromised contractor credentials, infostealer-derived, no-MFA customer tenants) was analysed in exactly this grammar, as were the OAuth-app findings of the 2023–2024 CI-platform audits; regulators now write it down explicitly (NIS2 third-party clauses, EU CRA’s chain-of-accountability language). The architectural descendants are everywhere: token exchange (RFC 8693) so integrations mint per-operation short-lived tokens instead of holding standing ones; workload-identity federation in the big clouds for the same reason; vaulted-then-decrypted-in-use credential stores with rotation telemetry; and app-identity anomaly detection as a first-class auth-analytics signal. Defenders in 2026 do their annual OAuth-app inventories, scope-minimise standing grants, and demand vendor attestations about where tokens live — all of it traceable to the week a platform’s environment variables turned into the JavaScript ecosystem’s worst supply-chain scare since SolarWinds.
Detection and hardening takeaways
- Vault long-lived integration tokens. Any OAuth/app credential your platform stores for third parties belongs in a dedicated secrets vault: encryption at rest, decryption only in short use-windows, access-logged, rotation-scheduled — so a platform compromise reads one dead encrypted blob, not a cleartext skeleton key.
- Prefer token exchange over storage. Architect integrations on RFC 8693-style exchange: the long-term credential never leaves the vault; per-operation, audience-scoped, short-TTL tokens are minted and die quickly — the stolen-token-to-graph-access path becomes a dead-end by construction.
- Scope grants like they’re production access — they are. Authorisation-time is when blast radius is chosen; default to least scopes, expire standing grants, require re-consent for scope growth. In 2022 the app’s broad default scopes turned one vendor breach into org-wide reach; modern reviews demand granular, per-repo/per-org scope models.
- Baseline app-identity behaviour. Detect anomalies keyed on the OAuth app identity: which grants it exercises, from which IPs, at what rate, against which orgs — GitHub’s postmortems noted the vendor-app tokens acted against unusual orgs; the signal existed, and app-identity analytics now standardly page on it.
- Protect the CI/caching layer like production. Heroku’s assessed entry was a token leaked via a cached CI environment; ephemeral-build-cache hygiene (no credential persistence, per-run secret isolation, cache scrubbing) closes the cheapest door attackers find.
FAQ
Was Heroku itself “hacked” in the usual sense?
Its platform infrastructure was compromised, yes — but the valuable loot wasn’t Heroku customer data (app code and config vars were assessed unexposed); it was the OAuth tokens Heroku held on behalf of the GitHub integration. That inversion is the lesson: the breached party and the harmed parties were different populations, connected only by a delegated-credential trust pattern.
If my org never used the Heroku integration, was I affected?
Not by the token theft directly. The wide net caught orgs that had authorised the Heroku Dashboard/CI apps. The indirect exposure — npm packages whose manifest-tokens leaked — affected maintainers regardless of Heroku use, which is why the incident’s cleanup crossed the whole ecosystem even though its entry was narrow.
What’s “token exchange” in practice?
Instead of the integration holding a years-long OAuth token, it holds (or fetches) a credential that can only mint short-lived tokens for specific audiences — e.g. “read org X’s repos for the next 15 minutes”. A stolen exchange-credential is worthless without the vault’s protections; a minted token expires before it’s useful. Cloud workload-identity federation works this way, and it’s the pattern GitHub and major platforms converged on post-2022.
