Index /

The GitHub VS Code Extension Breach, in Threat-Model Terms

GitHub's internal repos got exfiltrated through a poisoned VS Code extension. Here's the threat model that actually matters — and what credential-storage architecture can and can't do about it.

DESIGN — NOT SHIPPED. This post describes planned or aspirational features. None of the commands, flags, or APIs shown here are available yet. What does this mean?

Today, GitHub confirmed that an employee installed a poisoned Visual Studio Code extension and ~3,800 internal repositories went out the door. The actor — TeamPCP, previously linked to supply-chain attacks against Aqua’s Trivy, CheckMarx’s KICS, LiteLLM, and others — is asking $50,000 on a forum and threatening to leak. No customer data; no public repos.

This isn’t a post about how KPM “would have stopped” this. Nobody can honestly claim that without the post-mortem in front of them. It’s a post about which parts of the threat model credential-storage architecture can address, and which parts it can’t.

The attack pattern that actually shows up in the wild

Most credential-stealing extensions don’t do anything exotic. They scan known locations:

  • ~/.gitconfig, credential helpers, .netrc
  • ~/.aws/credentials, ~/.aws/config
  • ~/.npmrc, ~/.docker/config.json
  • .env files in any project the user has open
  • Browser cookie databases (for session theft)
  • Environment variables of the parent shell, via /proc on Linux or whatever’s available on macOS

Then they POST the contents to a server. That’s it. That’s the attack. It works because every one of those files is plaintext, the credentials inside them are long-lived, and the scope of each is usually “everything that token can do” — which, for a developer’s GitHub PAT, is often “every repo in every org I belong to.”

Whether this specific extension worked exactly that way isn’t public yet. But it’s the dominant pattern and it’s the relevant baseline.

Two layers that change the math

KPM, the project I’ve been writing about in this series, is built around two ideas that map directly onto this threat model.

Layer 1: ciphertext at rest. Secrets live on disk as AES-256-GCM ciphertext. The decryption key lives in a background listener bound to a UID-restricted Unix socket. When a process needs a secret, the listener decrypts and hands plaintext only to that child process, for the duration of its work. A rogue extension that scans ~/.gitconfig, .env, or any of the other known-location files gets gibberish. The entire “scan-known-paths” class of attack evaporates.

That alone is most of the day-to-day benefit, and it’s available with the simplest possible KPM setup.

Layer 2: short-lived, scoped credentials. This is where the “~3,800 repos” number gets interesting. Instead of a developer carrying around a long-lived org-wide PAT, AgentKMS — the policy and issuance side of KPM — mints credentials on demand, narrowly scoped, with TTLs measured in minutes. The admin credential never leaves the server. The developer never holds anything that grants more than the next short window of one specific operation.

Even granting an attacker the worst-case scenario — code execution as the developer’s UID, full access to KPM via the legitimate socket — the blast radius is bounded by what the next policy-evaluated credential request can yield. Not by what’s already sitting on disk.

What KPM doesn’t claim to do

Same-UID code execution is same-UID code execution. If you can run code as the developer, you can:

  • Request credentials through KPM, within whatever policy allows for that machine and context.
  • Read the legitimate consumer’s process memory during the short window plaintext exists there.
  • Run git clone yourself and ride the user’s session.
  • Steal browser cookies, SAML tokens, or OAuth refresh tokens that live in the browser’s own stores.

KPM doesn’t pretend otherwise. The architecture isn’t “untrusted code can never get a credential.” It’s “untrusted code can never get a credential it wasn’t going to be issued anyway, and the credentials it can get are short-lived and narrowly scoped.”

The point is to make the worst case smaller. Three thousand eight hundred repos is the worst case when long-lived, org-wide PATs live in plaintext files on the developer’s laptop. It’s not the worst case when they don’t.

The threat model is the news

Extensions, AI agents, supply-chain attacks against dev tools — untrusted code running as your user is the new baseline, not the edge case. The credential architecture most organizations still run on — long-lived PATs in ~/.gitconfig, SSH keys with no passphrase, cloud creds in .env — was designed for a world where the developer’s laptop was approximately trustworthy. That world is gone.

GitHub is going to be fine. Their team is good and their response was textbook: isolate the endpoint, rotate, communicate. The interesting question isn’t what GitHub does differently. It’s what the rest of us do, given that this exact incident is going to happen to a smaller company without GitHub’s response capability sometime in the next twelve months.

If you’ve read the series, you already know the answer I’m working on. If you haven’t: Part 1 starts here. KPM is open-source at github.com/TheGenXCoder/kpm.


Sources