# GlassWorm teardown: malware hidden in invisible Unicode

> How GlassWorm hid code in invisible Unicode variation selectors across npm, GitHub and VS Code, how the loader decodes it, and what Chainsaw stops.

Source: https://chain305.com/teardowns/glassworm-hidden-unicode/

---

# 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.

By [Muhammad Zeeshan Sultan](https://chain305.com/about/#founder), founder of Chain305 · October 2026

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:

Characters

Unicode block

Byte value

U+FE00 to U+FE0F

Variation Selectors

0 to 15

U+E0100 to U+E01EF

Variation Selectors Supplement

16 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

-   Aikido Security, [Glassworm Is Back: A New Wave of Invisible Unicode Attacks Hits Hundreds of Repositories](https://www.aikido.dev/blog/glassworm-returns-unicode-attack-github-npm-vscode), 13 March 2026.
-   Aikido Security, [Glassworm Strikes Popular React Native Phone Number Packages](https://www.aikido.dev/blog/glassworm-strikes-react-packages-phone-numbers), 16 March 2026, updated 18 March 2026.
-   isotope¹³ compendium, [Glassworm March 2026 campaign](https://isotope13.ai/compendium/campaigns/glassworm-march-2026/).
-   Koi Security, [GlassWorm: First Self-Propagating Worm Using Invisible Code Hits OpenVSX Marketplace](https://web.archive.org/web/20260101202601/https://www.koi.ai/blog/glassworm-first-self-propagating-worm-using-invisible-code-hits-openvsx-marketplace), 18 October 2025 (archived).
-   Eclipse Foundation, [Open VSX security update, October 2025](https://blogs.eclipse.org/node/8583), 27 October 2025.

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.

[Subscribe to teardowns](https://chain305.com/teardowns/) [Read what a package firewall catches →](https://chain305.com/what-is-a-package-firewall/)
