Skip to content

Commit badaf20

Browse files
Introduce new .getSignedReferences() function of signature to help prevent signature wrapping attacks (#489)
1 parent d80e5b7 commit badaf20

10 files changed

+171
-63
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## 6.0.1 (2025-03-14)
44

5-
- Address CVEs: CVE-2025-29774 and CVE-2025-29775
5+
- Address CVEs: CVE-2025-29774 and CVE-2025-29775
66

77
---
88

README.md

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,18 @@
33
![Build](https://github.com/node-saml/xml-crypto/actions/workflows/ci.yml/badge.svg)
44
[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/from-referrer/)
55

6-
An xml digital signature library for node. Xml encryption is coming soon. Written in pure javascript!
6+
---
77

8-
For more information visit [my blog](http://webservices20.blogspot.com/) or [my twitter](https://twitter.com/YaronNaveh).
8+
# Upgrading
9+
10+
The `.getReferences()` AND the `.references` APIs are deprecated.
11+
Please do not attempt to access them. The content in them should be treated as unsigned.
12+
13+
Instead, we strongly encourage users to migrate to the `.getSignedReferences()` API. See the [Verifying XML document](#verifying-xml-documents) section
14+
We understand that this may take a lot of efforts to migrate, feel free to ask for help.
15+
This will help prevent future XML signature wrapping attacks.
16+
17+
---
918

1019
## Install
1120

@@ -161,6 +170,11 @@ var select = require("xml-crypto").xpath,
161170
var xml = fs.readFileSync("signed.xml").toString();
162171
var doc = new dom().parseFromString(xml);
163172

173+
// DO NOT attempt to parse whatever data object you have here in `doc`
174+
// and then use it to verify the signature. This can lead to security issues.
175+
// i.e. BAD: parseAssertion(doc),
176+
// good: see below
177+
164178
var signature = select(
165179
doc,
166180
"//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']",
@@ -177,39 +191,21 @@ try {
177191
In order to protect from some attacks we must check the content we want to use is the one that has been signed:
178192

179193
```javascript
180-
// Roll your own
181-
const elem = xpath.select("/xpath_to_interesting_element", doc);
182-
const uri = sig.getReferences()[0].uri; // might not be 0; it depends on the document
183-
const id = uri[0] === "#" ? uri.substring(1) : uri;
184-
if (
185-
elem.getAttribute("ID") != id &&
186-
elem.getAttribute("Id") != id &&
187-
elem.getAttribute("id") != id
188-
) {
189-
throw new Error("The interesting element was not the one verified by the signature");
194+
if (!res) {
195+
throw "Invalid Signature";
190196
}
197+
// good: The XML Signature has been verified, meaning some subset of XML is verified.
198+
var signedBytes = sig.getSignedReferences();
191199

192-
// Get the validated element directly from a reference
193-
const elem = sig.references[0].getValidatedElement(); // might not be 0; it depends on the document
194-
const matchingReference = xpath.select1("/xpath_to_interesting_element", elem);
195-
if (!isDomNode.isNodeLike(matchingReference)) {
196-
throw new Error("The interesting element was not the one verified by the signature");
197-
}
200+
var authenticatedDoc = new dom().parseFromString(signedBytes[0]); // Take the first signed reference
201+
// It is now safe to load SAML, obtain the assertion XML, or do whatever else is needed.
202+
// Be sure to only use authenticated data.
203+
let signedAssertionNode = extractAssertion(authenticatedDoc);
204+
let parsedAssertion = parseAssertion(signedAssertionNode);
198205

199-
// Use the built-in method
200-
const elem = xpath.select1("/xpath_to_interesting_element", doc);
201-
try {
202-
const matchingReference = sig.validateElementAgainstReferences(elem, doc);
203-
} catch {
204-
throw new Error("The interesting element was not the one verified by the signature");
205-
}
206+
return parsedAssertion; // This the correctly verified signed Assertion
206207

207-
// Use the built-in method with a an xpath expression
208-
try {
209-
const matchingReference = sig.validateReferenceWithXPath("/xpath_to_interesting_element", doc);
210-
} catch {
211-
throw new Error("The interesting element was not the one verified by the signature");
212-
}
208+
// BAD example: DO not use the .getReferences() API.
213209
```
214210

215211
Note:

src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
export { C14nCanonicalization, C14nCanonicalizationWithComments } from "./c14n-canonicalization";
2+
export {
3+
ExclusiveCanonicalization,
4+
ExclusiveCanonicalizationWithComments,
5+
} from "./exclusive-canonicalization";
16
export { SignedXml } from "./signed-xml";
2-
export * from "./utils";
37
export * from "./types";
8+
export * from "./utils";

src/signed-xml.ts

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,30 @@
11
import type {
22
CanonicalizationAlgorithmType,
3+
CanonicalizationOrTransformAlgorithmType,
34
CanonicalizationOrTransformationAlgorithm,
5+
CanonicalizationOrTransformationAlgorithmProcessOptions,
46
ComputeSignatureOptions,
7+
ErrorFirstCallback,
58
GetKeyInfoContentArgs,
69
HashAlgorithm,
710
HashAlgorithmType,
811
Reference,
912
SignatureAlgorithm,
1013
SignatureAlgorithmType,
1114
SignedXmlOptions,
12-
CanonicalizationOrTransformAlgorithmType,
13-
ErrorFirstCallback,
14-
CanonicalizationOrTransformationAlgorithmProcessOptions,
1515
} from "./types";
1616

17-
import * as xpath from "xpath";
17+
import * as isDomNode from "@xmldom/is-dom-node";
1818
import * as xmldom from "@xmldom/xmldom";
19-
import * as utils from "./utils";
19+
import * as crypto from "crypto";
20+
import { deprecate } from "util";
21+
import * as xpath from "xpath";
2022
import * as c14n from "./c14n-canonicalization";
21-
import * as execC14n from "./exclusive-canonicalization";
2223
import * as envelopedSignatures from "./enveloped-signature";
24+
import * as execC14n from "./exclusive-canonicalization";
2325
import * as hashAlgorithms from "./hash-algorithms";
2426
import * as signatureAlgorithms from "./signature-algorithms";
25-
import * as crypto from "crypto";
26-
import * as isDomNode from "@xmldom/is-dom-node";
27+
import * as utils from "./utils";
2728

2829
export class SignedXml {
2930
idMode?: "wssecurity";
@@ -71,6 +72,14 @@ export class SignedXml {
7172
*/
7273
private references: Reference[] = [];
7374

75+
/**
76+
* Contains the canonicalized XML of the references that were validly signed.
77+
*
78+
* This populates with the canonical XML of the reference only after
79+
* verifying the signature is cryptographically authentic.
80+
*/
81+
private signedReferences: string[] = [];
82+
7483
/**
7584
* To add a new transformation algorithm create a new class that implements the {@link TransformationAlgorithm} interface, and register it here. More info: {@link https://github.com/node-saml/xml-crypto#customizing-algorithms|Customizing Algorithms}
7685
*/
@@ -87,6 +96,8 @@ export class SignedXml {
8796
"http://www.w3.org/2000/09/xmldsig#enveloped-signature": envelopedSignatures.EnvelopedSignature,
8897
};
8998

99+
// TODO: In v7.x we may consider deprecating sha1
100+
90101
/**
91102
* To add a new hash algorithm create a new class that implements the {@link HashAlgorithm} interface, and register it here. More info: {@link https://github.com/node-saml/xml-crypto#customizing-algorithms|Customizing Algorithms}
92103
*/
@@ -96,6 +107,8 @@ export class SignedXml {
96107
"http://www.w3.org/2001/04/xmlenc#sha512": hashAlgorithms.Sha512,
97108
};
98109

110+
// TODO: In v7.x we may consider deprecating sha1
111+
99112
/**
100113
* To add a new signature algorithm create a new class that implements the {@link SignatureAlgorithm} interface, and register it here. More info: {@link https://github.com/node-saml/xml-crypto#customizing-algorithms|Customizing Algorithms}
101114
*/
@@ -252,6 +265,7 @@ export class SignedXml {
252265
this.signedXml = xml;
253266

254267
const doc = new xmldom.DOMParser().parseFromString(xml);
268+
255269
// Reset the references as only references from our re-parsed signedInfo node can be trusted
256270
this.references = [];
257271

@@ -298,34 +312,62 @@ export class SignedXml {
298312
this.loadReference(reference);
299313
}
300314

315+
/* eslint-disable-next-line deprecation/deprecation */
301316
if (!this.getReferences().every((ref) => this.validateReference(ref, doc))) {
317+
/* Trustworthiness can only be determined if SignedInfo's (which holds References' DigestValue(s)
318+
which were validated at this stage) signature is valid. Execution does not proceed to validate
319+
signature phase thus each References' DigestValue must be considered to be untrusted (attacker
320+
might have injected any data with new new references and/or recalculated new DigestValue for
321+
altered Reference(s)). Returning any content via `signedReferences` would give false sense of
322+
trustworthiness if/when SignedInfo's (which holds references' DigestValues) signature is not
323+
valid(ated). Put simply: if one fails, they are all not trustworthy.
324+
*/
325+
this.signedReferences = [];
302326
if (callback) {
303327
callback(new Error("Could not validate all references"), false);
304328
return;
305329
}
306330

331+
// We return false because some references validated, but not all
332+
// We should actually be throwing an error here, but that would be a breaking change
333+
// See https://www.w3.org/TR/xmldsig-core/#sec-CoreValidation
307334
return false;
308335
}
309336

310-
// Stage B: Take the signature algorithm and key and verify the SignatureValue against the canonicalized SignedInfo
337+
// (Stage B authentication step, show that the `signedInfoCanon` is signed)
338+
339+
// First find the key & signature algorithm, these should match
340+
// Stage B: Take the signature algorithm and key and verify the `SignatureValue` against the canonicalized `SignedInfo`
311341
const signer = this.findSignatureAlgorithm(this.signatureAlgorithm);
312342
const key = this.getCertFromKeyInfo(this.keyInfo) || this.publicCert || this.privateKey;
313343
if (key == null) {
314344
throw new Error("KeyInfo or publicCert or privateKey is required to validate signature");
315345
}
316346

317-
if (callback) {
318-
signer.verifySignature(unverifiedSignedInfoCanon, key, this.signatureValue, callback);
347+
// Check the signature verification to know whether to reset signature value or not.
348+
const sigRes = signer.verifySignature(unverifiedSignedInfoCanon, key, this.signatureValue);
349+
if (sigRes === true) {
350+
if (callback) {
351+
callback(null, true);
352+
} else {
353+
return true;
354+
}
319355
} else {
320-
const verified = signer.verifySignature(unverifiedSignedInfoCanon, key, this.signatureValue);
356+
// Ideally, we would start by verifying the `signedInfoCanon` first,
357+
// but that may cause some breaking changes, so we'll handle that in v7.x.
358+
// If we were validating `signedInfoCanon` first, we wouldn't have to reset this array.
359+
this.signedReferences = [];
321360

322-
if (verified === false) {
361+
if (callback) {
362+
callback(
363+
new Error(`invalid signature: the signature value ${this.signatureValue} is incorrect`),
364+
);
365+
return; // return early
366+
} else {
323367
throw new Error(
324368
`invalid signature: the signature value ${this.signatureValue} is incorrect`,
325369
);
326370
}
327-
328-
return true;
329371
}
330372
}
331373

@@ -442,6 +484,7 @@ export class SignedXml {
442484
elem = elemOrXpath;
443485
}
444486

487+
/* eslint-disable-next-line deprecation/deprecation */
445488
for (const ref of this.getReferences()) {
446489
const uri = ref.uri?.[0] === "#" ? ref.uri.substring(1) : ref.uri;
447490

@@ -525,6 +568,11 @@ export class SignedXml {
525568

526569
return false;
527570
}
571+
// This step can only be done after we have verified the `signedInfo`.
572+
// We verified that they have same hash,
573+
// thus the `canonXml` and _only_ the `canonXml` can be trusted.
574+
// Append this to `signedReferences`.
575+
this.signedReferences.push(canonXml);
528576

529577
return true;
530578
}
@@ -655,6 +703,7 @@ export class SignedXml {
655703
if (nodes.length === 0) {
656704
throw new Error(`could not find DigestValue node in reference ${refNode.toString()}`);
657705
}
706+
658707
if (nodes.length > 1) {
659708
throw new Error(
660709
`could not load reference for a node that contains multiple DigestValue nodes: ${refNode.toString()}`,
@@ -771,8 +820,17 @@ export class SignedXml {
771820
});
772821
}
773822

774-
getReferences(): Reference[] {
775-
return this.references;
823+
/**
824+
* @deprecated Use `.getSignedReferences()` instead.
825+
* Returns the list of references.
826+
*/
827+
getReferences = deprecate(
828+
() => this.references,
829+
"getReferences() is deprecated. Use `.getSignedReferences()` instead.",
830+
);
831+
832+
getSignedReferences() {
833+
return [...this.signedReferences];
776834
}
777835

778836
/**
@@ -1007,6 +1065,7 @@ export class SignedXml {
10071065
prefix = prefix || "";
10081066
prefix = prefix ? `${prefix}:` : prefix;
10091067

1068+
/* eslint-disable-next-line deprecation/deprecation */
10101069
for (const ref of this.getReferences()) {
10111070
const nodes = xpath.selectWithResolver(ref.xpath ?? "", doc, this.namespaceResolver);
10121071

test/document-tests.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ describe("Document tests", function () {
2121
const result = sig.checkSignature(xml);
2222

2323
expect(result).to.be.true;
24+
expect(sig.getSignedReferences().length).to.equal(1);
2425
});
2526

2627
it("test with a document (using StringKeyInfo)", function () {
@@ -39,6 +40,7 @@ describe("Document tests", function () {
3940
const result = sig.checkSignature(xml);
4041

4142
expect(result).to.be.true;
43+
expect(sig.getSignedReferences().length).to.equal(1);
4244
});
4345
});
4446

@@ -51,10 +53,13 @@ describe("Validated node references tests", function () {
5153
sig.loadSignature(sig.findSignatures(doc)[0]);
5254
const validSignature = sig.checkSignature(xml);
5355
expect(validSignature).to.be.true;
56+
expect(sig.getSignedReferences().length).to.equal(1);
5457

58+
/* eslint-disable-next-line deprecation/deprecation */
5559
const ref = sig.getReferences()[0];
5660
const result = ref.getValidatedNode();
5761
expect(result?.toString()).to.equal(doc.toString());
62+
expect(sig.getSignedReferences().length).to.equal(1);
5863
});
5964

6065
it("should not return references if the document is not validly signed", function () {
@@ -64,10 +69,13 @@ describe("Validated node references tests", function () {
6469
sig.loadSignature(sig.findSignatures(doc)[0]);
6570
const validSignature = sig.checkSignature(xml);
6671
expect(validSignature).to.be.false;
72+
expect(sig.getSignedReferences().length).to.equal(0);
6773

74+
/* eslint-disable-next-line deprecation/deprecation */
6875
const ref = sig.getReferences()[1];
6976
const result = ref.getValidatedNode();
7077
expect(result).to.be.null;
78+
expect(sig.getSignedReferences().length).to.equal(0);
7179
});
7280

7381
it("should return `null` if the selected node isn't found", function () {
@@ -78,7 +86,9 @@ describe("Validated node references tests", function () {
7886
sig.loadSignature(sig.findSignatures(doc)[0]);
7987
const validSignature = sig.checkSignature(xml);
8088
expect(validSignature).to.be.true;
89+
expect(sig.getSignedReferences().length).to.equal(1);
8190

91+
/* eslint-disable-next-line deprecation/deprecation */
8292
const ref = sig.getReferences()[0];
8393
const result = ref.getValidatedNode("/non-existent-node");
8494
expect(result).to.be.null;
@@ -92,12 +102,15 @@ describe("Validated node references tests", function () {
92102
sig.loadSignature(sig.findSignatures(doc)[0]);
93103
const validSignature = sig.checkSignature(xml);
94104
expect(validSignature).to.be.true;
105+
expect(sig.getSignedReferences().length).to.equal(1);
95106

107+
/* eslint-disable-next-line deprecation/deprecation */
96108
const ref = sig.getReferences()[0];
97109
const result = ref.getValidatedNode(
98110
"//*[local-name()='Attribute' and @Name='mail']/*[local-name()='AttributeValue']/text()",
99111
);
100112
expect(result?.nodeValue).to.equal("[email protected]");
113+
expect(sig.getSignedReferences().length).to.equal(1);
101114
});
102115

103116
it("should return `null` if the selected node isn't validly signed", function () {
@@ -107,11 +120,15 @@ describe("Validated node references tests", function () {
107120
sig.loadSignature(sig.findSignatures(doc)[0]);
108121
const validSignature = sig.checkSignature(xml);
109122
expect(validSignature).to.be.false;
123+
expect(sig.getSignedReferences().length).to.equal(0);
110124

125+
/* eslint-disable-next-line deprecation/deprecation */
111126
const ref = sig.getReferences()[0];
112127
const result = ref.getValidatedNode(
113128
"//*[local-name()='Attribute' and @Name='mail']/*[local-name()='AttributeValue']/text()",
114129
);
115130
expect(result).to.be.null;
131+
// Not all references verified, so no references should be in `.getSignedReferences()`.
132+
expect(sig.getSignedReferences().length).to.equal(0);
116133
});
117134
});

0 commit comments

Comments
 (0)