Press enter or click to view image in full size
Trendyol CSOC and Application Security research on a live supply-chain technique abusing Cursor and other VS Code-derived editors through the OpenVSX registry.
TOC
- How we found ourselves looking at this
- Measuring the blast radius
- How Cursor decides what to install
- What changed in June 2025
- Walking through an attack, step by step
- Indicators you can track
- Hunting for it
- What you should do this week
- Where this goes next
If your developers run Cursor, VSCodium, Windsurf, or any other fork of VS Code that pulls its extensions from OpenVSX, attacker controlled code is one editor launch away from executing on their machines. None of this is hypothetical, and the underlying idea is not even new. It is the same dependency confusion problem the npm world ran into in 2021, moved down a layer into the IDE’s extension system. We have spent the past month chasing it across our own developer fleet, watching OpenVSX namespace activity, and detonating samples in the lab. What follows is what we found, what we have done about it, and what we think every organisation should be doing this week.
TL;DR. Because Microsoft’s Marketplace and OpenVSX are separate trust roots, an attacker can register an abandoned or never claimed publisher.extension namespace on OpenVSX and have every Cursor, Windsurf, or other VS Code fork that resolves the same identifier silently install and run their code as the developer. The fixes that actually work are an organization wide extension allowlist, an auto update cooldown, and the detection logic at the end of this post.
We did not stop at the theory. Our team snapshotted both registries in full and compared all 129,000 Microsoft Marketplace extensions against the 14,194 published on OpenVSX, one identifier at a time. What came back is a map of how much of the ecosystem is already exposed:
- 104,456 Marketplace extensions have no entry on OpenVSX at all, which leaves every one of those names as a free, squat ready namespace on the registry the forks resolve against.
- 1,078 extensions resolve to a different publisher on OpenVSX than the one that legitimately owns the name on Microsoft’s Marketplace.
- 924 carry the same name but are already pulled from a different GitHub repository than the original.
- 126 are exact-ID hijacks: the identical publisher.extension exists on both registries, but the OpenVSX copy is wired to a different repository or account. This is the most dangerous band, and it includes names with millions of installs such as sumneko.lua, rust-lang.rust, and msazurermtools.azurerm-vscode-tools, where a developer typing the exact trusted identifier can still be handed code that is not the original.
The attack surface on OpenVSX is wider than most people assume, and the last few months have made that hard to ignore: GlassWorm, tracked by Socket’s researchers, is a cluster of extensions that go up looking harmless, sit as sleepers, then flip into malware through hijacked publisher accounts, even hiding their command-and-control in WebAssembly and Solana transaction memos. The namespace squatting we mapped is just one more door into the same building.
How we found ourselves looking at this
Our hunting team was reviewing developer telemetry as part of a routine sweep when we noticed something unusual in the process tree under Cursor. Cursor populates two environment variables on every process descended from an extension activation:
CURSOR_SPAWNED_BY_EXTENSION_ID=<publisher>.<extension>
CURSOR_SPAWN_CHAIN=<publisher>.<extension>Those variables turned out to be the cleanest anchor we have ever had for “find me everything an extension did”. Once we built that pivot, we saw classic recon and exfil verbs: whoami, hostname, id, environment dumps, git remote enumeration, and HTTPS POSTs to webhook listeners, all spawning from Cursor Helper (Plugin), all attributable to a specific publisher.extension we did not recognise.
We pulled the VSIX off the mirror, opened it in a contained environment, and watched it do exactly what the telemetry implied. Then we mapped the publisher namespace back to public repositories that referenced it, and back to its registration history on OpenVSX. The pattern was unambiguous. The identifier had been referenced in third-party templates and devcontainers for years, the original publisher account had lapsed, and somebody had registered a fresh account under the same name and shipped a new high-version VSIX. Every Cursor instance that resolved that identifier got the new payload.
To be explicit: nothing in our environment was compromised. The detonations happened in instrumented research workstations under Application Security control, and the namespace activity on OpenVSX is monitored from the outside. We are publishing because the same primitive is reachable against any organisation whose developers run an OpenVSX-backed editor, and the volume of squatted identifiers we are seeing now is too high to wait on a vendor fix.
To be fair to Cursor, this boundary is not unguarded. The marketplace proxy scans at search and download time and fails closed, threat-intel feeds block known-bad and typosquatted packages, and enterprise tiers add allowlisting, an install cooldown, optional signature verification, and a domain-verified publisher program. We have reflected those in the description above so the architecture is accurate. But accuracy cuts both ways, and none of it changes the finding. Every one of these controls is either an automated scan, which a staged or dormant payload is built to walk past, or an enterprise setting that is off until an admin turns it on. The squatter who owns the right name and waits is invisible to the first and unaffected by the second. So the residual risk does not go away; it just becomes the customer’s to carry, on an attack surface most of them do not yet know they are exposed to.
Measuring the blast radius
A single sample proves the technique works, but it says nothing about how much of the ecosystem is actually exposed. To get at that, we stopped looking at extensions one at a time and pulled both registries down in full. The entire Microsoft Visual Studio Marketplace, around 129,000 published extensions, against the entire OpenVSX registry, about 14,194, lined up identifier by identifier, publisher by publisher, repository by repository.
The gap is mostly empty space, and that turns out to matter. Roughly 104,456 marketplace identifiers have no entry of any kind on OpenVSX, and all of them belong to Microsoft-verified publishers. Each one is a free namespace on the registry Cursor resolves against. A developer who carries any of these across the migration flow, lists one in a .vscode/extensions.json, or pulls one in through a devcontainer is asking OpenVSX for a name nobody legitimate has claimed there, and if a squatter got there first, the squatter’s package is what comes back. That is a squat-ready surface six figures wide.
At the same time we found 1,078 identifiers where the same extension name is published on OpenVSX under a different publisher than the one that owns it on Microsoft’s marketplace, and another 924 where the name matches but the source repository behind it does not: 572 pointing at a completely different GitHub repo, 301 where the repository is present on one side and missing on the other, and 51 that are mixed or not GitHub at all. A repository mismatch under a matching name is about the cleanest mechanical tell we have that the package a developer thinks they are installing is not the one they get.
So that this is something a team can act on rather than just a big number, we scored the high-confidence cases and grouped them into a definite universe of 393 identifiers, organised by how the trust boundary actually breaks:
Press enter or click to view image in full size
- Exact-ID hijack (126). The identical publisher.extension lives on both registries, but the OpenVSX copy comes from an unrelated account or points at a different repository. Some of these are heavily used names: sumneko.lua (2.1M marketplace installs, OpenVSX copy backed by a different repo name), rust-lang.rust (1.8M), and msazurermtools.azurerm-vscode-tools (1.5M), whose OpenVSX namespace is unverified and wired to the throwaway octocat/Hello-World repo.
- Migration traps (377). A popular marketplace extension whose name is squatted on OpenVSX under an alternate identifier that a migrating or fuzzy-matching resolver can land on. KADEAI.kade, with 913K downloads on the squatting kade.kade, sits at the top.
- Squats on unrelated popular names (38). Low-download OpenVSX packages parked on the namespace of a heavily-installed marketplace extension. nguyenoss.html-to-css-autocompletion shadows a name with 1.28M marketplace installs on roughly 5,900 downloads, with ericc-ch.lit-html (418K), Nsttt.native-preview (356K), and vuluu2k.vscode-elixir (331K) following the same shape. We flagged all of these critical.
How Cursor decides what to install
First, a word on scope, because “VS Code fork” is doing a lot of work in this post. Microsoft open-sources the core of VS Code under the MIT licence as a project called Code-OSS, and a large family of editors is built on top of that codebase. When we say a VS Code derived editor, we mean any of them: the AI-first editors that have driven most of the recent growth, Cursor and Windsurf above all, alongside community and cloud builds. What ties them together is not the AI features but the plumbing underneath: they all inherit the same extension architecture, and almost none of them can ship Microsoft’s own marketplace with it. If your developers run any editor in that family, this applies to them, and the two that dominate enterprise developer fleets right now are Cursor and Windsurf.
The reason none of these can simply reuse Microsoft’s marketplace is licensing. VS Code reads from the Microsoft Visual Studio Marketplace, but the Marketplace Terms of Use restrict it to Microsoft products only: VS Code, Visual Studio, Azure DevOps. That clause is the reason every VS Code derived editor has to use OpenVSX instead, the open alternative operated by the Eclipse Foundation.
The two registries share the publisher.extension naming scheme, but they do not share ownership and they do not share contents. Owning acme on Microsoft’s marketplace does not give you acme on OpenVSX. If acme was never claimed there, or was claimed once and dropped, anyone can pick it up. Just as importantly, several of the most widely used extensions on Microsoft’s marketplace are not on OpenVSX at all because Microsoft’s terms forbid republishing them: the C# extension, Pylance, Remote-SSH, Live Share, and a handful of others.
Microsoft did not stop at the terms. In 2025 it started actively enforcing them, adding runtime checks to its own extensions so they refuse to run anywhere that is not a genuine Microsoft product. The C/C++ extension was the first high-profile case, with the C#, .NET, and Pylance tooling following the same path, and overnight the forks lost the ability to run Microsoft’s most depended-on extensions at all. That enforcement is what pushed the whole problem into the open. Cut off from the originals, Cursor and the other forks had to lean on OpenVSX for everything and republish the gaps themselves. Cursor fills those gaps by packaging their own forks under the Anysphere publisher (anysphere.pyright is a Pylance fork; anysphere.remote-ssh replaces Microsoft’s Remote-SSH; the same pattern applies to several more). The official replacements are the ones that show up first in Cursor’s extension search. Anything that is not on OpenVSX and not republished by Anysphere lives in a hole that name-squatters are happy to fill.
Architecturally, Cursor does not talk to open-vsx.org directly. The editor’s product.json sets extensionsGallery.serviceUrl to https://marketplace.cursorapi.com/_apis/public/gallery, which is Cursor’s own proxy layer in front of OpenVSX. The proxy applies automated analysis before an extension is served, and withholds anything that has not passed. Even with it in place, when the analysis comes back clean the package is still served under whatever publisher owns the name on OpenVSX, which may not be the party a developer believes they are trusting. The scan answers “is this payload malicious right now”; it does not answer “is this the publisher you meant”, and namespace confusion lives entirely inside that second question.
On a default install there is no signature continuity check and no warning when the upstream publisher behind a previously trusted identifier changes hands. Cursor does have controls that bear on this, an opt-in domain-verified publisher program, optional enterprise enforcement of signature verification, and centrally managed allowlisting through b IAM, but the ones that would actually pin a publisher or block a swapped identity are opt-in or enterprise-gated. Also Cursor uses 3rd parties to scan extensions and block them if necessary. For someone using the default experience, there is little indication that a familiar identifier may now be controlled by a different party. The Microsoft Marketplace’s signed publisher verification layer, which has historically caught at least the most blatant impersonation attempts, is on by default; here the equivalent protection is off until someone deliberately enables it, and the squat works in the meantime.
Every one of these paths feeds an identifier to the registry, accepts whatever the registry returns, and installs it. The two upstream entry points the Cursor proxy itself fronts are:
cursor-cdn.com/openvsx_v0/
marketplace.cursorapi.com/open-vsx-mirror/
What changed in June 2025
The exposure window for Cursor users opens with a specific, datable decision. On 25 June 2025, Cursor announced on their forum that the in-app extension library had been switched to use OpenVSX as the underlying source (forum thread). The post explained that the team would also begin republishing many popular extensions under their own Anysphere publisher namespace, framed as drop-in replacements for compatibility. From version 1.1.3 onward, switching the marketplace backend was technically possible but explicitly called out as not officially supported.
That single change rerouted every extension identifier the editor had ever resolved. Before the migration, a developer typing ms-python.python into Cursor’s extension search would land on the Microsoft-published package from the Visual Studio Marketplace, because that is where the namespace is owned. After the migration, the same query goes to OpenVSX, where the namespace may be owned by a different party, owned by Anysphere as a republish, or unclaimed entirely.
The most consequential exposure path is the migration flow that almost every Cursor user goes through on first install.
When a developer moves from VS Code to Cursor, they expect their extensions to come with them. Cursor offers exactly that: import the previous extension list and reinstall everything. The list is just identifiers. The editor reads each publisher.extension and asks the registry to resolve it. After the June 2025 change, that registry is OpenVSX. So a developer who had a perfectly legitimate set of Microsoft Marketplace extensions, every one of them properly owned by the right publisher there, ends up with whatever OpenVSX returns for those names. If a squatter owns the namespace on OpenVSX, the squatter’s payload is what gets installed. The developer made no mistake. They imported their own extension list. The trust boundary moved underneath them.
This is the registry-level mechanic that makes Extension Confusion a practical attack rather than a theoretical one. The class existed before the migration. Independent research from Aikido and Koi Security in 2024 had already documented malicious OpenVSX packages impersonating popular names like prettier, discord, and solidity, and pointed out that VS Code forks were structurally more exposed than the Microsoft Marketplace because they lack the signed publisher verification layer. What changed in June 2025 was the size of the user base feeding identifiers into that exposed registry. Cursor’s transition turned a known weakness in OpenVSX into an attack with mass-market reach.
Walking through an attack, step by step
Press enter or click to view image in full size
Step 1. The attacker picks a target identifier. The attacker is not guessing, and they are not trawling GitHub for stray config files. They are running the same comparison we ran for this research, just with the opposite goal. They take the Microsoft Marketplace, which is the authoritative list of which extensions developers actually use and how popular each one is, and they diff it against OpenVSX, the registry the forks resolve against. The gap between the two is the target list. They are looking for one of three patterns: a popular Marketplace extension whose publisher never claimed the same name on OpenVSX, one whose OpenVSX account was registered once and then lapsed, or one that is simply absent from OpenVSX because Microsoft’s terms forbid republishing it. Any of those is a free namespace attached to a name developers already trust and install. Sorting the gap by Marketplace install count hands the attacker a ready-made priority queue: the higher the install number, the more Cursor and Windsurf users will resolve that identifier without the attacker doing anything further. Targets like bracketpaircolordlw.bracket-pair-color-dlw, georgewfraser.vscode-javac, or inferrinizzard.prettier-sql-vscode fit exactly this shape: real extensions developers reference, not republished by Anysphere, with the original publisher no longer defending the namespace on OpenVSX.
Step 2. The attacker claims the namespace on OpenVSX. Anyone can do this. The attacker signs up to open-vsx.org with an email address, links a GitHub account, and requests a publisher namespace through the standard self-service flow. There is no signed identity check, no domain verification, no proof that the requester has any prior relationship to the name. Approval is administrative and fast. Within hours the attacker controls the publisher namespace.
Unfortunately, when an Open-VSX developer uploads a VSIX package, it’s unclear whether that person is the owner of the GitHub account listed on the official Microsoft Marketplace.
Step 3. The attacker builds a malicious VSIX. A VSIX file is a zip archive with a package.json manifest and JavaScript code. The manifest declares the same publisher.extension identifier as the squat target. It sets activationEvents to [“onStartupFinished”], which tells the editor to run the extension’s activate() function the next time the IDE finishes loading, with no workspace requirement and no user click. The main field points at extension.js, where the payload lives. The payload itself is unconstrained Node.js: filesystem access, child-process spawning, network calls, full environment access. Most of the samples we have analysed in this campaign do reconnaissance on activation, exfiltrate the result over HTTPS to a webhook listener, and exit. Other samples carry credential-stealing or persistence behaviour.
Step 4. The attacker bumps the version number and publishes. This is the part that turns the squat from a passive trap into an active attack. The legitimate version of inferrinizzard.prettier-sql-vscode is 1.6.0. The malicious VSIX is published as 20.1.0. From the registry’s perspective there is no rule that says version numbers should be sane. From Cursor’s perspective 20.1.0 is simply newer than 1.6.0, so any auto-update check will treat the malicious package as the upgrade. The attacker runs ovsx publish from the command line, the VSIX uploads to open-vsx.org, and the package becomes the canonical version of that identifier.
Step 5. The package reaches Cursor’s proxy. Cursor does not query open-vsx.org directly. The editor’s product.json points at https://marketplace.cursorapi.com/_apis/public/gallery, a Cursor-operated proxy in front of OpenVSX, served from cursor-cdn.com/openvsx_v0/ and marketplace.cursorapi.com/open-vsx-mirror/. This is the step where Cursor’s automated malware analysis runs, and a package that fails it is blocked. That catches the unsophisticated end of the problem, but it is exactly the kind of control attackers stage around: delayed activation, dynamic payload fetch, encrypted blobs, environment-gated and geofenced logic all exist to pass a clean scan and detonate later. A squatted namespace whose payload is dormant at publish time clears the proxy and reaches the developer like any other approved extension.
Step 6. The developer’s editor resolves the identifier. Resolution happens through one of several paths, none of which require the developer to do anything unusual. The most common is the migration flow: a developer installs Cursor for the first time, accepts the prompt to import their VS Code extensions, and Cursor walks the imported list calling its install endpoint for each publisher.extension. Each lookup hits the OpenVSX cache. If a squatter owns the namespace, the squatter’s VSIX comes back. The developer thinks they are reinstalling the same extensions they have always used. The other paths look the same from the editor’s perspective: opening a workspace whose .vscode/extensions.json lists the identifier, starting a devcontainer with the identifier in customizations.vscode.extensions, running an internal onboarding script that calls cursor — install-extension publisher.ext, or simply having auto-update enabled on an existing install while the squatter publishes a higher version number.
Step 7. Cursor installs and activates the VSIX. The download lands in the user’s ~/.cursor/extensions directory under a folder named <publisher>.<extension>-<version>. The editor reads package.json, sees “activationEvents”: [“onStartupFinished”], and as soon as the IDE finishes its startup sequence, calls activate(context). There is no security prompt, no signature verification, and no banner indicating the package is new or that the publisher behind a previously trusted name has changed. The developer’s IDE looks normal.
Step 8. The payload runs as the developer. At this point there is no security boundary left. The extension is a Node.js process with the full privileges of the user account running Cursor. It can read every file in every workspace folder, walk every Git repository, dump environment variables, harvest credentials from ~/.aws, ~/.ssh, ~/.kube, and ~/.netrc, exfil the contents of any open editor buffer, spawn child processes that persist past the IDE’s lifetime, hook into terminals, and reach any internal network the developer’s laptop can reach.
The attack ends here, in the sense that the attacker now has whatever they wanted from the developer’s environment. The exposure does not end here, because the same primitive runs again on every IDE launch, and the same identifier resolves the same malicious version on every other developer who imports a VS Code extension list, opens the same repository, or starts the same devcontainer. One published VSIX is enough for the rest of the supply chain to do the work.
Indicators you can track
It is also possible to see the proof-of-concepts created by some researchers:
c0c0c549e1d7adc014f0762cf2711d0e6e3fc8acf9a4f70f1bb16f8a9938c9f8
055f5058026df4ef754c33597497ea9b61d86e0f9bc2153de496534bd7a37b6f
3164fce8183a92672301dc947343a91a262e4503e3eb9358860386d040e42bfa
fdea6a2f7e959cc81aec5eeeed0bf6d50bc1475d78e79a408e729797ece4e734
The two environment variables that anchor everything for hunting:
CURSOR_SPAWNED_BY_EXTENSION_ID=<publisher>.<extension>
CURSOR_SPAWN_CHAIN=<publisher>.<extension>
Hunting for it
Our detection model is built on three correlated signals: a child process whose parent is Cursor Helper (Plugin), a CURSOR_SPAWN* environment variable carrying an identifier that is not on our developer-extension allowlist, and either a reconnaissance command line or an outbound network request that does not match the editor’s known telemetry destinations.
The CrowdStrike Falcon search below implements that model. Translate the field names to whatever SIEM you run. The structure is the important part. The following query catches processes descending from Cursor’s extension host by looking for the CURSOR_SPAWN signature in their environment variables. The filtering layer targets only classic reconnaissance commands (id, whoami, hostname, pwd, git config); while these commands are harmless on their own, spawning from an extension’s activation chain is not expected behaviour. The regex block extracts the triggering extension’s identity and spawn chain into dedicated fields, so results can be filtered directly against an allowlist and every extension-to-command relationship is visible in a single row.
index=falcon event_simpleName=ProcessRollup2 EnvironmentVariablesString="*CURSOR_SPAWN*"
| search CommandLine IN (
"/bin/sh -c id",
"/bin/sh -c whoami",
"/bin/sh -c hostname",
"/bin/sh -c pwd",
"/bin/sh -c git config*")
| rex field=EnvironmentVariablesString "CURSOR_SPAWNED_BY_EXTENSION_ID=(?<extension_id>\S+)"
| rex field=EnvironmentVariablesString "CURSOR_SPAWN_CHAIN=(?<spawn_chain>\S+)"
| table _time, aid, ComputerName, ParentBaseFileName, extension_id, spawn_chain, ImageFileName, CommandLine, ComputerName, UserNameThe second query goes beyond the first by not stopping at command-line matching alone; it correlates DNS requests initiated by the extension host process with reconnaissance and exfiltration commands executed by the same process. This surfaces cases where an extension both runs a suspicious command and reaches out to a network destination outside the editor’s known telemetry targets, collapsing both signals into a single alert. The inclusion of git config, git remote, and git credential in the command list is deliberate; through these calls the attacker harvests the developer’s GitHub, GitLab, Bitbucket, and Azure DevOps identities, remote repository URLs, and stored credentials. Combined with the DNS correlation, this makes it visible in a single alert not only what the attacker enumerated locally but which external destination the results were exfiltrated to. The query depends on a lookup file called cursor_whitelisted_extensions.csv; this file must be created ahead of time and populated with your organisation’s approved third-party extensions and internally developed extension identifiers. Finally, the NOT DomainName IN (…) block in the opening line covers only baseline Microsoft and GitHub telemetry destinations; expanding this list with the CDNs, npm registries, and API endpoints that extensions in your environment legitimately call will help bring the false-positive rate down to a workable level.
index=falcon event_simpleName=DnsRequest ContextBaseFileName="Cursor Helper (Plugin)" NOT DomainName IN ("www.microsoft.com", "*google.com", "go.dev", "aka.ms", "settings.visualstudio.microsoft.com", "copilot-telemetry.githubusercontent.com")
[ search index=falcon event_simpleName=ProcessRollup2 EnvironmentVariablesString="*CURSOR_SPAWN*"
| rex field=EnvironmentVariablesString "CURSOR_SPAWNED_BY_EXTENSION_ID=(?<extension_id>\S+)"
| rex field=EnvironmentVariablesString "CURSOR_SPAWN_CHAIN=(?<spawn_chain>\S+)"
| search CommandLine IN ("*whoami*", "*hostname*", "*systeminfo*", "*ipconfig*", "*ifconfig*", "*ip addr*", "*ip route*", "*netstat*", "*ps aux*", "*ps -ef*", "*tasklist*", "*net user*", "*net localgroup*", "*net group*", "*cat /etc/passwd*", "*cat /etc/shadow*", "*cat /etc/hosts*", "*printenv*", "*arp -a*", "*nslookup*", "*dig *", "*sw_vers*", "*system_profiler*", "*security find*", "*dscl *", "*curl *", "*wget *", "*urllib*", "*nc -*", "*ncat *", "*netcat*", "*base64*", "*openssl *", "*certutil*", "*bitsadmin*", "*Invoke-WebRequest*", "*WebClient*", "*osascript*", "*scp *", "*rsync *", "*/bin/id*", "* id", "uname*", "*git config*", "*git remote*", "*git credential*")
| lookup cursor_whitelisted_extensions.csv extension_id OUTPUT extension_id AS whitelisted
| where isnull(whitelisted)
| eval ContextProcessId = tostring(SourceProcessId)
| fields aid, ContextProcessId]
| eval ContextProcessId = tostring(ContextProcessId)
| join type=inner max=0 aid ContextProcessId
[ search index=falcon event_simpleName=ProcessRollup2 EnvironmentVariablesString="*CURSOR_SPAWN*"
| rex field=EnvironmentVariablesString "CURSOR_SPAWNED_BY_EXTENSION_ID=(?<extension_id>\S+)"
| rex field=EnvironmentVariablesString "CURSOR_SPAWN_CHAIN=(?<spawn_chain>\S+)"
| lookup cursor_whitelisted_extensions.csv extension_id OUTPUT extension_id AS whitelisted
| where isnull(whitelisted)
| eval ContextProcessId = tostring(SourceProcessId)
| fields aid, ContextProcessId, extension_id, spawn_chain, CommandLine, ParentBaseFileName, ImageFileName, ComputerName, UserName]
| mvexpand extension_id
| mvexpand spawn_chain
| mvexpand CommandLine
| table aid, ComputerName, UserName, extension_id, spawn_chain, ParentBaseFileName, ContextBaseFileName, ImageFileName, DomainName, CommandLine, ContextProcessIdTwo things to call out about the search. The DNS allowlist at the top is the IDE’s known telemetry, marketplace, and copilot destinations. Anything outside that set, originating from the extension host, is the start of the alert. The command-line block is intentionally broad because we are anchoring on extension-host provenance, not on the verbs themselves. The allowlists for trusted developer extensions and known-benign command lines are environment-specific. Replace eamodio.gitlens, anthropic.claude-code, and our internal-tooling exclusions with whatever your fleet has approved. Do not weaken the verb list.
For teams that do not run Falcon, the same idea reduces to:
parent_process == “Cursor Helper (Plugin)”
AND environment contains “CURSOR_SPAWNED_BY_EXTENSION_ID”
AND extension_id NOT IN allowlist
AND (command_line MATCHES recon_or_credential_pattern OR dns_query NOT IN known_telemetry_allowlist OR network_destination MATCHES (webhook.site, oast.pro, *.ngrok.io, interact.sh, requestbin.*, beeceptor.com))Before any of this works, the telemetry has to exist, and that is the part we nearly got wrong. Several EDR products do not, in their default configuration, record the files an extension drops on disk. The gap is worst on macOS, which is exactly where most Cursor and Windsurf users are. When a malicious VSIX unpacks into ~/.cursor/extensions, ~/.vscode/extensions, or the Windsurf equivalents, the agent watches a .js or .ts file land but never opens it or hashes it. What you get is a file-creation event with a path and a timestamp and nothing underneath it to pivot on: no hash to match against known-bad, nothing to retro-hunt with across the rest of the fleet, no SHA-256 to hand IR. The events are there; the one field detection actually needs is not.
The fix is to add those directories to your file integrity or content collection policy on developer endpoints, so that every .js, .ts, and .vsix written under them is hashed and the hash is shipped with the event. While you are in that policy, confirm the same endpoints capture full command lines and environment blocks for processes spawned under Cursor Helper (Plugin). If the CURSOR_SPAWN* variables never reach your process events, the queries above return nothing and the silence reads as “clean” when it is really “blind”. Treat both gaps as configuration findings, not as evidence that nothing is happening, and re-baseline once they are closed.
What you should do this week
Four things, in order of return on effort.
1- Allowlist your extensions. Maintain an organisation-wide list of approved publisher.extension identifiers and enforce it through MDM, IDE Admin configuration, or a managed extension proxy. Nothing else on this list matters as much. If you run Cursor or Windsurf on their enterprise tiers, you do not have to build this from scratch, because both ship a native allowlist control. Cursor exposes an allowed-extensions setting in its enterprise IAM admin panel that restricts installs to a list you define centrally. Windsurf does the equivalent through its enterprise policies, which let an administrator control extension management across the fleet. Turn one of these on before you do anything else; an enforced allowlist is the single control that takes the squat-ready surface off the table for your developers.
2- Disable or cooldown auto-update. If a full disable is impractical, hold new extension versions for 24 to 48 hours before they are eligible to install. Most malicious extensions get pulled inside that window, so a cooldown turns a race condition into a review opportunity. Cursor ships this natively: the Marketplace Install Cooldown (hours) setting defers installing or updating an extension until its current Marketplace version has been published for at least the number of hours you specify (set to 0 to disable). Configured at the team level, it overrides the per-user extensions.installCooldownHours value in each member's settings.json, so no developer can quietly opt their own machine back into instant updates.
3- Hunt the extensions your developers have actually installed. Do not stop at what policy says should be there; go look at what is on the machines. Collect the installed extension list from every developer endpoint, by reading the ~/.cursor/extensions, ~/.vscode/extensions, and Windsurf install directories through your EDR or MDM, and check each publisher.extension against OpenVSX. Anything that resolves to a publisher who does not own the name on Microsoft’s Marketplace, points at a different repository, was registered or re-registered recently, or has no track record is a squatting candidate and gets pulled and reviewed.
4- Treat developer endpoints as production-equivalent. Source code, deployment tokens, cloud credentials, customer data: all of it is sitting on developer machines. Apply EDR, alerting, response, and forensic standards. The CURSOR_SPAWN* anchor described above should be a permanent detection in every fleet that runs Cursor, not a one-time hunt.
Where this goes next
None of this is exotic, and that is the uncomfortable part. The weakness we exploited has been sitting in plain sight for years. As an industry we have leaned harder and harder on open-source dependencies and on the registries that distribute them, while quietly assuming somebody else was checking that the name we asked for still pointed at the code we trusted. Most of the time nobody was. The npm dependency-confusion work in 2021 said this out loud; the steady drip of malicious packages on npm, PyPI, and OpenVSX since has been saying it ever since. Extension Confusion is not a new failure so much as the same neglected one, surfacing on a package layer that happened to gain a few million new users overnight. The lesson is not that IDE extensions are uniquely dangerous. It is that any dependency surface you do not actively govern will, sooner or later, be governed by whoever wants it most, and right now that is the attacker.
About Us
We’re building a team of the brightest minds in our industry. Interested in joining us? Visit the pages below to learn more about our open positions.