The VSCode extension that robbed GitHub
I have at least forty VS Code extensions installed right now. I have not audited a single one of them. I installed most of them in under ten seconds by clicking a green button in a marketplace panel, and I could not tell you who maintains half of them or what they do with my filesystem.
That used to feel like a mild, acceptable risk - a little cognitive debt, nothing serious. The GitHub breach confirmed it is a more expensive debt than most people realized.
What actually happened
One GitHub employee installed a trojanized version of a popular developer extension. The attacker group TeamPCP had poisoned the Nx Console extension - a widely-used tool for monorepo management in JavaScript projects - and distributed the malicious version through the official VS Code Marketplace.
Once on the developer’s machine, the extension exfiltrated credentials. GitHub confirmed the result: roughly 3,800 internal repositories accessed and stolen.
GitHub’s statement was blunt: “Yesterday we detected and contained a compromise of an employee device involving a poisoned VS Code extension. We removed the malicious extension version, isolated the endpoint, and began incident response immediately.”
Their current assessment is that no customer data was affected - only GitHub-internal repos. TeamPCP, meanwhile, posted the data on the Breached cybercrime forum three days later with a price tag.
“As always this is not a ransom. We do not care about extorting Github. 1 buyer and we shred the data on our end… we are not interested in under 50k.”
Not a ransom. Got it.
--> // making it invisible to querySelectorAll. // // `data-cfasync="false"` keeps this rescue script executable even when // Rocket Loader is active. It rescues module scripts via two strategies: // 1. Query the DOM for type$="-module" + src (covers case A) // 2. Regex-parse the raw HTML for commented-out script tags (covers case B) // Dynamically-created scripts bypass Rocket Loader entirely. (function () { if (window.__markdyRescue) return; window.__markdyRescue = true; var rescued = false; function rescueModuleScripts() { if (rescued) return; rescued = true; var srcs = []; // Strategy 1: Rocket Loader kept the tag in DOM but changed the type. // type="module" → type="{uuid}-module" (still has src attribute) document.querySelectorAll('script[type$="-module"][src]').forEach(function (s) { srcs.push(s.src); }); // Strategy 2: Rocket Loader COMMENTED OUT the script tag entirely: // // These are invisible to querySelectorAll, so we parse the raw HTML. // We handle both attribute orderings (type-first or src-first). var html = document.documentElement.innerHTML; var reSrcFirst = //g; var reTypeFirst = //g; var m; while ((m = reSrcFirst.exec(html)) !== null) { srcs.push(m[1]); } while ((m = reTypeFirst.exec(html)) !== null) { srcs.push(m[1]); } // Re-inject each found src as a real module script. // Deduplicate first, then inject. Dynamically-created scripts bypass // Rocket Loader entirely. Modules with the same URL are only executed // once by the browser (cached), so re-injecting already-running scripts // is safe. var seen = {}; srcs.forEach(function (src) { if (seen[src]) return; seen[src] = true; var fix = document.createElement('script'); fix.type = 'module'; fix.src = src; fix.setAttribute('data-cfasync', 'false'); document.head.appendChild(fix); }); } // Rescue when user clicks the placeholder (fallback if autoplay failed). document.addEventListener('click', function (e) { var t = e.target; if (t && typeof t.closest === 'function' && t.closest('.markdy-placeholder')) { rescueModuleScripts(); } }); // Rescue automatically after a short delay for autoplay. // Only fires if initAll() never ran (no data-markdy-init on any root). setTimeout(function () { if (document.querySelector('.markdy-root:not([data-markdy-init])')) { rescueModuleScripts(); } }, 1500); }());Why this story climbed to 1,000 upvotes in a day
The attack chain is grimly elegant:
Legitimate extension (Nx Console)
↓
Supply-chain compromise by TeamPCP
↓
Malicious version pushed to VS Code Marketplace
↓
GitHub employee installs update
↓
Token/credential exfiltration
↓
~3,800 internal repos accessed
↓
Data listed on Breached forum for $50K
The reason this resonated wasn’t shock - it’s that it happened to GitHub, the platform that hosts most of the world’s code, via a developer tool that their own engineers use every day.
TeamPCP wasn’t new at this either. They were already linked to the TanStack npm supply-chain attack and a campaign called “Mini Shai-Hulud” that reportedly also compromised two OpenAI employees. This isn’t opportunistic hacking; it’s methodical developer-tool targeting.
This is the scenario the security community has warned about for years: the supply chain doesn’t just mean your dependencies - it means every piece of software in your development environment.
--> // making it invisible to querySelectorAll. // // `data-cfasync="false"` keeps this rescue script executable even when // Rocket Loader is active. It rescues module scripts via two strategies: // 1. Query the DOM for type$="-module" + src (covers case A) // 2. Regex-parse the raw HTML for commented-out script tags (covers case B) // Dynamically-created scripts bypass Rocket Loader entirely. (function () { if (window.__markdyRescue) return; window.__markdyRescue = true; var rescued = false; function rescueModuleScripts() { if (rescued) return; rescued = true; var srcs = []; // Strategy 1: Rocket Loader kept the tag in DOM but changed the type. // type="module" → type="{uuid}-module" (still has src attribute) document.querySelectorAll('script[type$="-module"][src]').forEach(function (s) { srcs.push(s.src); }); // Strategy 2: Rocket Loader COMMENTED OUT the script tag entirely: // // These are invisible to querySelectorAll, so we parse the raw HTML. // We handle both attribute orderings (type-first or src-first). var html = document.documentElement.innerHTML; var reSrcFirst = //g; var reTypeFirst = //g; var m; while ((m = reSrcFirst.exec(html)) !== null) { srcs.push(m[1]); } while ((m = reTypeFirst.exec(html)) !== null) { srcs.push(m[1]); } // Re-inject each found src as a real module script. // Deduplicate first, then inject. Dynamically-created scripts bypass // Rocket Loader entirely. Modules with the same URL are only executed // once by the browser (cached), so re-injecting already-running scripts // is safe. var seen = {}; srcs.forEach(function (src) { if (seen[src]) return; seen[src] = true; var fix = document.createElement('script'); fix.type = 'module'; fix.src = src; fix.setAttribute('data-cfasync', 'false'); document.head.appendChild(fix); }); } // Rescue when user clicks the placeholder (fallback if autoplay failed). document.addEventListener('click', function (e) { var t = e.target; if (t && typeof t.closest === 'function' && t.closest('.markdy-placeholder')) { rescueModuleScripts(); } }); // Rescue automatically after a short delay for autoplay. // Only fires if initAll() never ran (no data-markdy-init on any root). setTimeout(function () { if (document.querySelector('.markdy-root:not([data-markdy-init])')) { rescueModuleScripts(); } }, 1500); }());What HN is actually arguing about
The top comment that aged well came from user fg137: “The (lack of) security of VSCode has always been astounding. People have asked for sandboxing extensions for years with little to no progress.”
The reply thread went on for a while. The frustration has been building since at least 2018, when developers started raising issues about extensions having unrestricted filesystem and network access. VS Code extensions run as Node.js processes with full system privileges - no capability restrictions, no permission prompts, no sandboxing. You install the extension; it can read files, make network requests, spawn processes.
notnullorvoid put it simply: “I really hope this pushes Microsoft to add an explicit permission system to VS Code extensions, and improve security of dev containers.”
They’re right, but it’s wishful thinking. Firefox had the same conversation about browser extensions for years before Manifest v3. Change happens slowly until something goes badly wrong for someone big enough. GitHub is big enough.
The other angle the thread explored: how does a malicious extension exfiltrate credentials without triggering network monitoring? For most developers, the answer is: it doesn’t need to. Your IDE has outbound network access. Always has.
User gus_ asked the more uncomfortable question: “So how did they exfiltrate the information without noticing? What OS was the developer using? What security measures were they using?”
Nobody answered it because nobody knows, and GitHub hasn’t said.
Should you audit your extensions?
| Question | Practical answer |
|---|---|
| Should I check what extensions I have installed? | Yes, right now |
| Should I remove extensions I don’t actively use? | Yes |
| Does “popular” mean “safe”? | No - Nx Console had plenty of installs |
| Is the official marketplace safe? | Safer than random, but not safe |
| Should I pin extension versions? | At minimum, review before updating |
| Will VS Code add sandboxing soon? | The issue is open since 2018. You do the math. |
The thing that actually changes your risk is treating your IDE like a production server: not everything that has a green “Install” button needs to run on your machine with full credentials.
The one sentence GitHub wishes it could take back
GitHub’s own statement contains this line: “The attacker’s current claims of ~3,800 repositories are directionally consistent with our investigation so far.”
Directionally consistent. Corporate for: we believe them, we just haven’t finished counting.
The deeper irony is that GitHub is a platform designed to manage and secure source code for millions of organizations. They have Dependabot, secret scanning, code review, branch protection, audit logs. All of that infrastructure - and the breach came in through one developer’s extension manager.
Security is a chain and you are the weakest link on your own machine.
Discussion on Hacker News · Source: bleepingcomputer.com · Submitted by Timofeibu
Hoang Yell
A software developer and technical storyteller. I read Hacker News every day and retell the best stories here — in English and Vietnamese — for curious people who don't have time to scroll.