GitHub
GitHub is a Git hosting and CI platform whose accounts, tokens, tags, and workflows decide what gets built and published across open source.
Also known as GitHub.com, GH
GitHub is a hosted platform for Git repositories, code review, and automation through GitHub Actions, used by more than 180 million developers. In the software supply chain it is more than a code host: GitHub accounts, tokens, tags, and workflows decide what gets built and published, so compromising any of them can ship malicious code under a trusted name.
Four roles that used to belong to different systems now sit inside one GitHub organization: the source of truth, the identity provider, the build system, and, through trusted publishing, the authority that npm and PyPI accept for a release. GitHub also runs npm itself: the company announced on March 16, 2020 that npm was joining GitHub. The single most important fact about GitHub for supply chain security is that almost every name you depend on there is a mutable pointer. Only a full commit SHA is fixed, and even a SHA says nothing about who wrote it. Our position: treat your GitHub organization as production infrastructure. Any credential that can write to a repository can usually publish from it.
What is GitHub?
#GitHub is a service that hosts Git repositories and layers accounts, pull requests, CI, package hosting, and security advisories on top of them. GitHub's Octoverse 2025 report counts more than 180 million developers and 630 million repositories, 395 million of them public or open source, for the year ending August 31, 2025.
The platform has three layers that matter here:
- Git storage and names. Repositories, branches, tags, forks, and owner names such as
actions/checkout. - Identity and automation. User accounts, organizations, tokens, GitHub Apps, and GitHub Actions, which runs workflows with access to repository secrets. The details of workflow hardening live in our entry on GitHub Actions security.
- Ecosystem services. The npm registry, the GitHub Advisory Database that
npm auditreads, and the OpenID Connect (OIDC) tokens that registries accept as proof that a release came from a given workflow.
A supply chain attack against GitHub rarely targets GitHub's own code. It targets the trust that one of these layers extends to another.
Git content is immutable; GitHub names are not
#Git stores every file, directory, and commit as an object named by the hash of its content. A commit SHA therefore fixes the entire tree it points to, plus its history. Change one byte and you get a different SHA.
A Git ref is the opposite. A branch or tag is a name that holds a SHA, and anyone allowed to push can point it somewhere else. Tags in particular look like versions, but nothing in Git stops a maintainer, or whoever holds the maintainer's token, from moving one. You can see the convention on any popular action:
$ git ls-remote https://github.com/actions/checkout refs/tags/v4 refs/tags/v4.4.0 refs/tags/v6 refs/tags/v6.1.0
11d5960a326750d5838078e36cf38b85af677262 refs/tags/v4
11d5960a326750d5838078e36cf38b85af677262 refs/tags/v4.4.0
d23441a48e516b6c34aea4fa41551a30e30af803 refs/tags/v6
d23441a48e516b6c34aea4fa41551a30e30af803 refs/tags/v6.1.0We ran this on November 3, 2026. The major tag v4 resolves to the same commit as v4.4.0, and v6 to the same commit as v6.1.0. Maintainers move major tags forward on every release by design, so a workflow that says uses: actions/checkout@v4 runs whatever v4 points to at the moment the job starts.
Attackers inherit that convention. In March 2025, the tj-actions/changed-files compromise repointed tags v1 through v45.0.7 at one malicious commit that printed CI secrets into workflow logs. It is tracked as CVE-2025-30066, and more than 23,000 repositories used the action. In September 2026, the re-enabled actions-cool actions showed the same mechanism with no new attack at all: GitHub had disabled actions-cool/issues-helper and actions-cool/maintain-one-comment during the Mini Shai-Hulud campaign in May, and when both came back on September 16, their release tags still pointed at the malicious code. For issues-helper alone, GitHub's dependency graph listed about 15,000 dependent repositories.
When we tried to fetch the disabled repository on November 3, 2026, Git refused before returning any refs:
$ git ls-remote https://github.com/actions-cool/issues-helper
fatal: could not read Username for 'https://github.com': terminal prompts disabledA full SHA closes the tag problem but not every problem. GitHub's documentation on forks states that "commits pushed to any repository in a network can be accessible from other repositories in that network" (GitHub Docs). A commit pushed to a fork can therefore be fetched through the upstream repository's URL, so uses: owner/action@<sha> can resolve to a commit the upstream maintainer never wrote. These are called impostor commits. A SHA proves immutability, not provenance.
The figure shows the two failure modes side by side. Today refs/tags/v4 and refs/tags/v4.4.0 both point to commit 11d5960a…, the commit a reviewer approved. A git push --force by anyone holding tag rights can repoint v4 to an attacker commit, and every workflow that references @v4 runs the new commit on its next job. Separately, a commit pushed to any fork in the repository's network can be fetched through the upstream URL, so a pinned SHA can still name code the upstream maintainer never wrote.
The same split between names and objects shows up when a package manager installs straight from GitHub. Here is what npm records for a Git dependency:
$ npm install --package-lock-only --ignore-scripts github:jonschlinkert/is-number#7.0.0
$ jq '.dependencies' package.json
{ "is-number": "github:jonschlinkert/is-number#7.0.0" }
$ jq '.packages["node_modules/is-number"]' package-lock.json
{
"version": "7.0.0",
"resolved": "git+ssh://git@github.com/jonschlinkert/is-number.git#98e8ff1da1a89f93d1397a24d7413ed15421c139",
"license": "MIT",
"engines": { "node": ">=0.12.0" }
}The manifest names a tag. The lockfile pins the commit SHA, but it has no integrity field, unlike a registry tarball entry. A fresh npm install without the lockfile follows the tag to wherever it points that day. As of npm v12, released in July 2026, Git dependencies no longer resolve unless you allow them with --allow-git.
Owner names are mutable too. When a user or organization renames or deletes an account, someone else can register the old name and recreate the repository that old references still fetch, which is repojacking.
| Reference | Example | Who can change what it resolves to | Verdict |
|---|---|---|---|
| Branch | uses: owner/action@main | Anyone with push access to the branch | Never use for third-party code |
| Tag | uses: actions/checkout@v4 | Anyone with tag push rights, unless the tag belongs to an immutable release | A human label, not a pin |
| Full commit SHA | uses: actions/checkout@11d5960a… | Nobody, but a fork-network commit can still resolve | Default for every third-party action |
| SHA plus workflow lockfile | Public preview since October 14, 2026 | Nobody; the lockfile also pins nested actions and checks the commit is on a branch | Layer on top of SHA pinning |
| Git dependency | github:owner/repo#7.0.0 | The tag mover; the lockfile records a SHA but no integrity hash | Avoid in published packages |
| Owner name | owner/repo itself | Whoever registers a freed name | Prefer numeric repository IDs where tools expose them |
GitHub's newer workflow lockfiles, in public preview since October 14, 2026, record the exact commit behind every action reference, nested ones included, and refuse a locked commit that is not on any branch, which is the property an impostor commit lacks (our hardening guide covers the limits).
Verdict: reference third-party GitHub code by full commit SHA, enforce it with policy rather than review, and treat every tag and owner name as a label a stranger might control tomorrow.
Which GitHub credentials can publish your code?
#Any GitHub credential with write access to a repository can usually publish from it. Write access lets the holder change a workflow, and a workflow can read every secret the repository stores, including registry tokens. That makes the question "which credentials exist?" a supply chain question.
GitHub added type prefixes to its tokens in March 2021, which is why secret scanners can recognize them, and why attackers' regexes can too. The GhostAction wave in October 2026 shipped patterns for ghp_ and github_pat_ alongside AWS and AI keys.
| Prefix | Credential | Supply chain notes |
|---|---|---|
| ghp_ | Personal access token (classic) | Broad scopes across every repository the user can reach. Still required for some outside-collaborator writes. |
| github_pat_ | Fine-grained personal access token | Scoped to chosen repositories and permissions. Generally available since March 18, 2025. |
| gho_ | OAuth app access token | Carries whatever scopes a user granted a third-party app. |
| ghu_ | GitHub App user-to-server token | Acts as a user through an app. |
| ghs_ | GitHub App server-to-server token | The GITHUB_TOKEN in every Actions job is one of these. |
| ghr_ | GitHub App refresh token | Mints new ghu_ tokens. |
| none (JWT) | Actions OIDC ID token | Exchanged for short-lived registry publish tokens in trusted publishing. |
Three details in that table cause most real incidents. First, the GITHUB_TOKEN is a GitHub App installation token created per job; on GitHub-hosted runners it lives up to six hours. Its default permissions became read-only only for organizations and personal repositories created after February 2, 2023. Older organizations keep read and write unless someone changes the setting. Second, classic PATs still exist even though fine-grained tokens have been generally available since March 18, 2025, because fine-grained tokens cannot yet do everything; GitHub's documentation says only classic tokens have write access to public repositories you do not own or belong to. Third, bot accounts tend to hold long-lived PATs with broad write access, and the PostHog chain below started with exactly that.
The npm side has already moved. npm disabled new classic tokens in early November 2025 and revoked the rest on December 9, 2025. GitHub's own classic personal access tokens remain available, so the long-lived credential that matters most has moved one hop upstream, from the registry to the repository.
How do attackers use GitHub in supply chain attacks?
#Attackers use GitHub as an entry point into a project, as a pivot from source access to registry credentials, as a distribution channel, and as free infrastructure for exfiltration. The Shai-Hulud 2.0 compromise of PostHog shows the first two in one chain, documented in PostHog's post-mortem.
PostHog's post-mortem names the root of the chain in one line: "The first step the attacker took was to steal the Github Personal Access Token of one of our bots."
| Time | Layer | Event |
|---|---|---|
| Sep 11, 2025 | Repository | A merged change switches the auto-assign workflow to pull_request_target and runs assign-reviewers.js from the pull request head. |
| Nov 18, 17:40 (time zone not stated) | Actions | A now-deleted account opens a pull request that edits the script to send the job's secrets, including a bot's PAT, to a webhook. The PR is opened, run, and closed within about a minute. |
| Nov 18 to Nov 23 | Identity | The stolen token is not used for five days. |
| Nov 23, 15:28 UTC | Identity | The attacker tests the token by deleting a workflow run. |
| Nov 23, 15:43 | Repository | The attacker pushes a commit directly to the repository, with no pull request, editing the "Lint PR" workflow to exfiltrate all GitHub secrets. |
| Nov 23, 15:45 UTC | Actions | The modified workflow runs as posthog-bot, the secrets including the npm publish token are exfiltrated, and the run is deleted. PostHog later found the pivot in the GitHub audit log. |
| Nov 24, 04:11 UTC | npm | Malicious PostHog SDK versions are published with a preinstall payload. |
| Nov 24, 09:30 UTC | npm | PostHog deletes the packages and revokes the tokens used to publish them. |
Read the chain by layer and the lesson is uncomfortable. No step attacked npm. The registry did exactly what it should with a valid token. Every decisive step happened inside GitHub: a workflow trigger that trusted fork code, a bot PAT with broad write access, a repository that accepted a direct push from that bot, and a workflow that could read the npm token.
The same patterns show up across 2025 and 2026:
- Entry through workflows. The Nx s1ngularity compromise in August 2025 began with a pull request title injected into a bash step of a
pull_request_targetworkflow, and ended with the npm token inpublish.ymlposted to a webhook. - Stolen accounts committing workflows. In the GhostAction wave, two compromised maintainer accounts committed
.github/workflows/security-audit.ymlto 346 repositories on October 8, 2026, including an Uber-owned repository one of them could write to. By October 9, Socket had identified more than 500 accounts committing the same workflow. - GitHub as attacker infrastructure. The original Shai-Hulud worm of September 2025 exfiltrated credentials into a public repository it created on the victim's own account. Its November successor searched public GitHub for the phrase "Sha1-Hulud: The Second Coming." to recover tokens from earlier victims, and triple-base64-encoded its loot, which our analysis found was meant to slip past GitHub's built-in secret scanning.
- Git as the distribution channel. PolinRider compromises developer accounts, force-pushes rewritten history, and lets ecosystems that resolve directly from Git, such as Go modules and Packagist, carry the payload without any registry credential.
The last pattern has a structural consequence for defenders. An egress allowlist that permits github.com also permits a push to the attacker's repository on github.com. Network controls work on hosts, and GitHub is one host for everyone.
How do you secure a GitHub repository?
#You secure a GitHub repository by limiting what each identity, token, and workflow can do, and by making every external reference immutable. Each control below closes one link in the chain above and leaves others open.
Require phishing-resistant two-factor authentication on every account with write access, including bots. This stops password and one-time-code phishing. It does nothing about a token that already exists, which is how PostHog was entered, or a stolen browser session. Our entry on maintainer account takeover covers those routes.
Replace classic PATs with fine-grained tokens or GitHub Apps. Give each token one repository, the minimum permissions, and an expiry. Give bots a GitHub App installation instead of a user PAT, so their tokens expire after an hour. This shrinks the blast radius. It does not help when the one repository the token can reach is the one that publishes.
Default GITHUB_TOKEN to no permissions. Set the organization default to read-only, declare permissions: {} at the top of every workflow, and grant per job. Never check out or run pull request code in a pull_request_target job. GitHub's default policy blocking pull_request_target on public repositories without their own policy moves from evaluate mode to enforcement on December 2, 2026, as we covered in hardening GitHub Actions against outside threats.
Protect refs with rulesets, and keep publishing secrets out of repository scope. Block force-pushes, deletions, and updates on the default branch and on release tags, require reviewed pull requests, and assign .github/workflows/ to a security owner in CODEOWNERS. Tag rules protect everyone who consumes your actions. They do not protect your secrets: any user with write access can read every repository secret by pushing a workflow that uses them, which is the move PostHog's attacker made. Put registry credentials in a deployment environment restricted to protected branches and gated by required reviewers, so a workflow on an arbitrary branch cannot load them. Rulesets do not stop an administrator, and an attacker who steals an administrator's token inherits the bypass.
Pin third-party actions by full SHA, and enforce it. Since August 2025, the organization Actions policy can require SHA pinning, and "any workflow that attempts to use an action that isn't pinned will fail." Pinning does not reach nested actions inside a composite action, and it does not detect impostor commits from the fork network. Pair it with a release-age cooldown in your automated dependency-update bots, so a freshly retagged action is not adopted the day it changes.
Turn on secret scanning with push protection. GitHub turned on push protection by default for all user accounts in March 2024, after detecting more than one million leaked secrets in public repositories in the first eight weeks of that year. Secret scanning catches tokens committed by mistake. It cannot see a secret read from runner memory, or one encoded before it is pushed.
Separate building from publishing. Publish from a dedicated job that runs no project code, holds the only publishing credential, and sits behind a protected environment with required reviewers. Prefer trusted publishing, which replaces stored registry tokens with short-lived OIDC exchanges. This removes the long-lived token PostHog lost. It does not remove the workflow an attacker can still run.
Socket covers one specific gap here. When Socket scans a workflow, it parses the actions it references, including those declared inside composite actions, and analyzes the specific commit each tag resolves to at scan time. If the tag later moves to a different commit, the next scan flags the change (announcement). As of November 2026, Socket's ecosystem support page lists GitHub Actions scanning as experimental and available to Enterprise customers.
Provenance from GitHub is necessary but not sufficient
#Package provenance and trusted publishing both make the same promise: this artifact was built by this workflow in this repository. That promise is valuable. It defeats an attacker who steals a token and publishes from a laptop.
It does not defeat an attacker who runs code inside the workflow. In the TanStack compromise of May 2026, attacker-controlled code ran during the test and cleanup phase, extracted the OIDC token from the runner's memory, and published 84 malicious package artifacts through the project's legitimate trusted-publisher binding. No npm token was stolen. Our analysis found the payload could also submit Sigstore provenance attestations, which verify correctly and point at the attacker-controlled run.
That is the open problem with GitHub as a supply chain platform. One organization now holds the source, the build, the identities, and the authority a registry accepts for a release, so one write-capable credential can cross all four. Trusted publishing narrowed the credential, but it kept the decision inside the same trust domain that was compromised. The fix we think matters is a second, independent approval at the registry, made by a human with 2FA, outside the pipeline that pushed the code. npm's staged publishing, in public preview since May 20, 2026, does exactly this: a new version waits in staging until a maintainer approves it through a 2FA challenge. It is opt-in and must be enabled per package, and npm plans to remove direct publishing from 2FA-bypass tokens around January 2027. Until staged approval is the default, assume every GitHub credential that can write to your repository is a credential that can publish your package.
