ANT-2026-110HTZ5M · jsrsasign
other high
Severity Claude high · Security research firm high · Maintainer -
Anthropic's analysis, sealed at approval. Disclosure to the maintainer was performed by Calif.
ANT-2026-110HTZ5M: jsrsasign npm — ECDSA nonce RNG seeded entirely from Math.random() in Node.js
The npm wrapper at lib/jsrsasign.js:2-5 declares a module-local window = {} with no .crypto, so the seed-pool initialiser in ext/rng.js falls through to filling all 256 ARC4 key bytes from 128 Math.random() calls plus Date.now(). Every ECDSA nonce k (ecdsa-modified-1.0.js:243), generated EC private key, DSA nonce, RSA-PSS salt, and RSA prime produced by jsrsasign in Node is therefore a deterministic function of V8's xorshift128+ state at require() time. An attacker who observes a few Math.random() outputs leaked by any co-resident code (request-ID middleware, jitter, shuffles, non-secure UUIDs) can solve the xorshift128+ state, rewind to the pool-fill window, bound the load timestamp, replay the ARC4 keystream, and recover the signing nonce k. From one observed ECDSA signature (r,s) on a known message, the private key follows directly as d = (s·k − e)·r⁻¹ mod n.
Target
Project: jsrsasign
Version: ≤ 11.1.1 (npm distribution)
Location: ext/rng.js:28-58; npm/lib/jsrsasign.js:2-5; src/ecdsa-modified-1.0.js:61,98-103,235-252
Discovery: static analysis — not yet dynamically reproduced
Technical Details
Root cause is the var window = {} shim in the npm build: ext/rng.js checks only window.crypto/window.msCrypto (both undefined on the stub) and never checks globalThis.crypto.getRandomValues (present in Node ≥19) or require('crypto').randomBytes (zero grep hits in the package). The while (rng_pptr < rng_psize) loop therefore always executes in Node, deriving the entire 256-byte ARC4 seed from Math.floor(65536 * Math.random()) — V8's non-cryptographic xorshift128+ — XORed with two correlated millisecond timestamps. This predictable seed flows into new SecureRandom() → getBigRandom(n) → the ECDSA nonce k in signHex.
Reproduction
- Collect ~5 leaked Math.random() doubles from victim responses (request IDs, Retry-After jitter, shuffled arrays, non-secure UUIDs).
- Solve V8 xorshift128+ internal state from the doubles via linear algebra over GF(2) / Z3 (milliseconds; public tooling).
- Rewind the linear generator by 128+Δ calls to the require()-time pool-fill window, accounting for V8's reversed 64-entry cache.
- Iterate ~10³–10⁶ candidate Date.now() values for the two rng_seed_time() XORs; re-key ARC4 for each and generate candidate nonces k.
- Capture one ECDSA signature (r,s) on known hash e; for each candidate k compute d = (s·k − e)·r⁻¹ mod n and check against the public key.
- On match, attacker holds the private key; forge arbitrary signatures/JWTs for the lifetime of that key.
[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]
Suggested Fix
In ext/rng.js, before the Math.random() fallback loop, check for globalThis.crypto?.getRandomValues (Node ≥19, all modern browsers) or require('crypto').randomBytes when process.versions.node is defined. Alternatively/additionally, adopt RFC 6979 deterministic nonce derivation for ECDSA/DSA to remove RNG dependence from signing. Until fixed, Node users should not use jsrsasign for key generation or ECDSA/DSA signing.
Acknowledgement
This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-110HTZ5M.
Reference: ANT-2026-110HTZ5M
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure
Triage and disclosure were performed by Calif.
- Verdict
- true positive
- Severity
- high
Dates from discovery through public reveal.
- 2026-04-12 Sent to maintainer
- 2026-04-12 Patch released
- 2026-05-19 Reported to tracker
- 2026-05-28 Maintainer acknowledged
- 2026-08-17 Publicly revealed
SHA-3-512 hash:
16201330990313937c8b3a3daafb87057c94bbbed5c303c3b4c4da71a857d78c6775deb7527010b042ee68486a5a15a95a4ec8fa13319ecad7b6714567b11031
Committed 2026-05-28 08:09 PT
Revealed 2026-08-17 13:01 PT
Verify (download preimage.json)
Show preimage JSON
{
"ant_id": "ANT-2026-110HTZ5M",
"bug_class": "Cryptographically Weak PRNG / Predictable ECDSA Nonce",
"claude_severity": "high",
"commit_sha": null,
"created_at": "2026-05-20T01:48:55+00:00",
"description": "The npm wrapper at lib/jsrsasign.js:2-5 declares a module-local `window = {}` with no `.crypto`, so the seed-pool initialiser in ext/rng.js falls through to filling all 256 ARC4 key bytes from 128 Math.random() calls plus Date.now(). Every ECDSA nonce k (ecdsa-modified-1.0.js:243), generated EC private key, DSA nonce, RSA-PSS salt, and RSA prime produced by jsrsasign in Node is therefore a deterministic function of V8's xorshift128+ state at require() time. An attacker who observes a few Math.random() outputs leaked by any co-resident code (request-ID middleware, jitter, shuffles, non-secure UUIDs) can solve the xorshift128+ state, rewind to the pool-fill window, bound the load timestamp, replay the ARC4 keystream, and recover the signing nonce k. From one observed ECDSA signature (r,s) on a known message, the private key follows directly as d = (s·k − e)·r⁻¹ mod n.",
"discovered_at": "2026-03-28T00:00:00+00:00",
"location": "ext/rng.js:28-58; npm/lib/jsrsasign.js:2-5; src/ecdsa-modified-1.0.js:61,98-103,235-252",
"poc_sha256": null,
"preimage_version": 1,
"project": "jsrsasign",
"reproduction": [
"1. Collect ~5 leaked Math.random() doubles from victim responses (request IDs, Retry-After jitter, shuffled arrays, non-secure UUIDs).",
"2. Solve V8 xorshift128+ internal state from the doubles via linear algebra over GF(2) / Z3 (milliseconds; public tooling).",
"3. Rewind the linear generator by 128+Δ calls to the require()-time pool-fill window, accounting for V8's reversed 64-entry cache.",
"4. Iterate ~10³–10⁶ candidate Date.now() values for the two rng_seed_time() XORs; re-key ARC4 for each and generate candidate nonces k.",
"5. Capture one ECDSA signature (r,s) on known hash e; for each candidate k compute d = (s·k − e)·r⁻¹ mod n and check against the public key.",
"6. On match, attacker holds the private key; forge arbitrary signatures/JWTs for the lifetime of that key."
],
"technical_details": "Root cause is the `var window = {}` shim in the npm build: ext/rng.js checks only `window.crypto`/`window.msCrypto` (both undefined on the stub) and never checks `globalThis.crypto.getRandomValues` (present in Node ≥19) or `require('crypto').randomBytes` (zero grep hits in the package). The `while (rng_pptr < rng_psize)` loop therefore always executes in Node, deriving the entire 256-byte ARC4 seed from `Math.floor(65536 * Math.random())` — V8's non-cryptographic xorshift128+ — XORed with two correlated millisecond timestamps. This predictable seed flows into `new SecureRandom()` → `getBigRandom(n)` → the ECDSA nonce k in `signHex`.",
"title": "jsrsasign npm — ECDSA nonce RNG seeded entirely from Math.random() in Node.js",
"vendor_severity": "high"
}