Teardown: GlassWorm, March 2026

Code you cannot see, in a string that looks empty.

How the March 2026 GlassWorm wave hid its loader in invisible Unicode characters, what it stole, and where Chainsaw would and would not have stopped it.

Questions and answers

What happened?

GlassWorm is a campaign that hides malicious code in invisible Unicode characters. In March 2026 the group behind GlassWorm pushed hidden code into GitHub repositories, npm packages and a VS Code extension. Aikido Security, which reported the wave on 13 March, dates the GitHub compromises to 3 to 9 March 2026. A GitHub code search for the decoder returned at least 151 repositories, and according to Aikido the count is low because many affected repositories were deleted first. The most-starred was pedronauck/reworm, with about 1,460 stars.

The malicious changes sat inside commits that looked routine: documentation tweaks, version bumps, small refactors and bug fixes. On 12 March the same technique appeared in npm and the VS Code marketplace:

  • npm: @aifabrix/miso-client@4.7.2, and @iflow-mcp/watercrawl-watercrawl-mcp versions 1.3.0 to 1.3.4.
  • VS Code extension: quartz.quartz-markdown-editor@0.3.0.

GlassWorm was not new: Koi Security first reported the campaign on 18 October 2025, after seven OpenVSX extensions were compromised on 17 October with the same invisible-character trick. The Eclipse Foundation, which runs Open VSX, revoked the leaked publishing tokens and removed the extensions. Koi and Eclipse disagree on two points: Koi counted 35,800 downloads and called GlassWorm a self-propagating worm, while Eclipse says the download figure includes bot traffic and that GlassWorm was “not a self-replicating worm in the traditional sense”.

The isotope¹³ compendium also groups two React Native packages, react-native-country-select and react-native-international-phone-number, published on 16 March, with the campaign. Aikido’s analysis shows a different technique: an obfuscated preinstall script, not invisible Unicode, and both are covered under “What ran?” below.

How does the hidden code work?

The GlassWorm payload is stored as invisible characters, one per byte, inside a string that looks empty, and a few lines of JavaScript decode the characters into code and run the result with eval(). According to Aikido, the characters come from two Unicode blocks:

CharactersUnicode blockByte value
U+FE00 to U+FE0FVariation Selectors0 to 15
U+E0100 to U+E01EFVariation Selectors Supplement16 to 255

Together the two blocks give 256 characters, one for every byte value from 0 to 255. Variation selectors exist to pick a glyph style for the preceding character and draw nothing themselves, so in an editor, a code review or GitHub’s diff view the payload looks like an empty string or blank lines. Koi’s screenshot of the infected CodeJoy extension shows a gap between two lines of code: the entire payload.

Aikido describes the loader in four steps:

  1. A helper function, s(), receives a template string that looks empty.
  2. s() walks the string’s code points. A character from U+FE00 to U+FE0F becomes codePoint - 0xFE00. A character from U+E0100 to U+E01EF becomes codePoint - 0xE0100 + 16. Anything else is dropped.
  3. The resulting numbers go through Buffer.from(...).toString('utf-8') to become a JavaScript string.
  4. The decoded string is passed to eval().

A defanged reconstruction of the decoder (the backticks would hold the invisible payload):

const s = v => [...v].map(w => (w = w.codePointAt(0),
  w >= 0xFE00 && w <= 0xFE0F ? w - 0xFE00 :
  w >= 0xE0100 && w <= 0xE01EF ? w - 0xE0100 + 16 : null)).filter(n => n !== null);
eval(Buffer.from(s(`…`)).toString('utf-8'));

The visible part is short and could pass as a utility function. The constants 0xFE00, 0xE0100 and 0xE01EF are what Aikido searched GitHub for.

What ran?

Aikido did not publish an analysis of the second stage for the March wave. It says that in earlier GlassWorm incidents the decoded code fetched a second-stage script through Solana and stole tokens, credentials and secrets. The isotope¹³ compendium lists the targets as tokens, cloud credentials, package-registry secrets and local configuration files on developer machines and CI runners.

Koi’s October 2025 analysis shows what that chain looked like:

  • A Solana dead drop. The malware read the memo field of transactions sent from a hard-coded Solana wallet. The memo held a base64-encoded link to the next stage, so the attacker could change the download address by posting a new transaction.
  • An encrypted payload. The next stage came back AES-256-CBC encrypted, with the key in the HTTP response headers.
  • A backup channel. The title of a Google Calendar event held a second base64-encoded link.
  • Credential and wallet theft. It took npm, GitHub, OpenVSX and Git credentials, and targeted 49 cryptocurrency wallet extensions.
  • Remote access. A module Koi calls ZOMBI added a SOCKS proxy and hidden VNC access to the infected machine.

The two React Native packages used the same Solana idea with different code. According to Aikido, react-native-country-select@0.3.91 and react-native-international-phone-number@0.11.8 added a preinstall script that ran node install.js. The script skipped systems with a Russian locale, read a Solana transaction memo to find stage two, decrypted stage three with AES-256-CBC, stayed on Windows machines through a scheduled task and a Run registry key, and stole crypto wallets, browser data, npm tokens and GitHub credentials. The compendium lists further versions of both packages published up to 18 March.

Would Chainsaw have stopped it?

Yes for the npm packages that carried the invisible-Unicode loader, on day 0, for installs that go through the Chainsaw proxy. The React Native packages depend on the release hold and the malware feed, and the VS Code extension and the GitHub repositories are outside what Chainsaw covers. This comes from our re-run of 58 attacks from 2026 against production Chainsaw (v0.22.118) on 19 October 2026:

  • The npm packages are refused on first download. A default rule refuses packages whose published source carries hidden Unicode: bidi-override characters and variation selectors. Chainsaw reads the package files the first time they are fetched, so the install fails before the loader can run. No malware feed listing is needed.
  • The React Native packages are not refused on their bytes. They carried an obfuscated preinstall script rather than hidden Unicode, so the hidden-Unicode rule does not apply. They rely on the release hold and the malware feed, which listed them about 78 hours after publish.
  • VS Code and OpenVSX extensions are outside Chainsaw’s surface. Chainsaw sits between package managers and registries. Extensions install and update through the editor and its marketplace, which does not pass through it.
  • GitHub repositories are outside it too. Code that arrives by git clone or a pull does not go through a package proxy.
  • Installs that bypass the proxy are not protected. A laptop or CI runner pointed straight at registry.npmjs.org gets the package. This is why Chainsaw also gates CI, pins laptops through MDM and blocks direct registry traffic at the network.

What should you do?

The GlassWorm packages to look for are @aifabrix/miso-client@4.7.2, @iflow-mcp/watercrawl-watercrawl-mcp 1.3.0 to 1.3.4, the VS Code extension quartz.quartz-markdown-editor@0.3.0, and the React Native packages react-native-country-select and react-native-international-phone-number published from 16 March 2026. The most urgent step is to treat any machine or CI runner that ran one of the packages as compromised and rotate the credentials GlassWorm is reported to take.

First, check whether you installed or merged any of it:

  1. Compare installed npm packages with the named versions. npm ls @aifabrix/miso-client @iflow-mcp/watercrawl-watercrawl-mcp react-native-country-select react-native-international-phone-number shows what your lockfile resolved.
  2. Compare editor extensions with the list. code --list-extensions --show-versions lists what VS Code has installed. Look for quartz.quartz-markdown-editor@0.3.0.
  3. Search your own repositories for the hidden payload. Search for the characters themselves, not for suspicious strings. A single U+FE0F after an emoji is normal; a long run of variation selectors is not. With ripgrep, for example: rg -l '[\x{FE00}-\x{FE0F}\x{E0100}-\x{E01EF}]{4,}'. Also search for the decoder constants 0xFE00, 0xE0100 and 0xE01EF.
  4. Review recent history on repositories you maintain, including force-pushes. The malicious commits were written to look like ordinary maintenance.

If any machine or CI runner ran one of these, treat it as compromised. Rotate the credential types GlassWorm is reported to take: npm tokens, GitHub tokens and Git credentials, OpenVSX publishing tokens, cloud credentials and other registry secrets. Move crypto wallet funds to new keys. For the React Native packages, the attacker servers listed by Aikido are 45.32.150.251 and 217.69.3.152, and the Windows persistence is a scheduled task named UpdateApp and a Run registry value named DPKCbbQ.

Then, for the next wave: don’t rely on reading the code. Visual review and standard linting do not show these characters, so run an automated check for invisible Unicode in CI and on the packages you install.

Sources

One teardown per notable attack

Get the next teardown by email

What happened, the install-path angle most coverage misses, and the rule that refuses it.