Problem
validateReference removes a leading # from a Reference's URI only when one is present, so a URI without one goes through the same ID lookup:
|
private validateReference(ref: Reference, doc: Document): string | undefined { |
|
const uri = ref.uri?.[0] === "#" ? ref.uri.substring(1) : ref.uri; |
|
let elem: xpath.SelectSingleReturnType = null; |
|
|
|
if (uri === "") { |
|
elem = xpath.select1("//*", doc); |
|
} else if (uri?.indexOf("'") !== -1) { |
|
// xpath injection |
|
throw new Error("Cannot validate a uri with quotes inside it"); |
|
} else { |
|
let num_elements_for_id = 0; |
|
for (const attr of this.idAttributes) { |
|
const tmp_elemXpath = `//*[@*[local-name(.)='${attr}']='${uri}']`; |
|
const tmp_elem = xpath.select(tmp_elemXpath, doc); |
|
if (utils.isArrayHasLength(tmp_elem)) { |
|
num_elements_for_id += tmp_elem.length; |
|
|
|
if (num_elements_for_id > 1) { |
|
throw new Error( |
|
"Cannot validate a document which contains multiple elements with the " + |
|
"same value for the ID / Id / Id attributes, in order to prevent " + |
|
"signature wrapping attack.", |
|
); |
|
} |
|
|
|
elem = tmp_elem[0]; |
|
ref.xpath = tmp_elemXpath; |
|
} |
|
} |
|
} |
As a result, <Reference URI="foo"> verifies against <item Id="foo"> in the same document, exactly as URI="#foo" would. It has worked this way since the first implementation. validateElementAgainstReferences does the same (L515). xml-crypto never writes such a URI when signing, and no fixture uses one.
XMLDSig 1.1 §4.4.3.2 defines a same-document reference as an empty URI or # followed by a fragment. Any other URI identifies a separate resource, and dereferencing it must produce an octet stream. URI="foo" is a relative URI for a resource named foo, not an element of the document being verified.
The element selected is the one #foo would select, so this is a conformance and interoperability problem, not a known bypass. A signature with URI="foo" that xml-crypto accepts would not verify in .NET or signxml.
Other implementations
- .NET
Reference.CalculateHashValue handles a missing URI, "" and #…, and throws UriNotResolved for anything else: Reference.cs#L384-L468
- Apache Santuario's in-document resolver,
ResolverFragment, accepts only "" and #… that isn't #xpointer(. Other URIs go to its other resolvers: ResolverFragment.java#L112-L126
- signxml handles
"", #xpointer( and #…. Any other URI needs a caller-supplied resolver and raises InvalidInput without one. It also rejects a Reference with no URI: processor.py#L153-L176
- xmlsec classifies a URI without a leading
# as local (file://) or remote and dereferences it externally, only if that URI type is enabled: transforms.c#L247-L260, transforms.c#L893-L913
- goxmldsig compares
URI[1:] with the ID, dropping the first character whatever it is. It is looser than xml-crypto: validate.go#L310, validate.go#L488
- node-saml copies xml-crypto's optional
# handling, then requires the referenced element to be the signature's parent, so the loose form gains nothing there: xml.ts#L117-L139
Related: a Reference without a URI
loadReference turns a missing URI attribute into undefined, and addReference defaults that to "", so the reference is verified as the whole document (L786-L797, L817). §4.4.3.1 says that when URI is omitted, the receiving application is expected to know the identity of the object. signxml rejects such a reference, and .NET passes no input to the transform chain. The same change should decide this case.
Proposal
Make reference validation fail with an explicit error when URI is neither empty nor starts with #, and decide how to handle a missing URI. This rejects documents that verify today, so it belongs in 7.0.
#589 already limits its comment removal to "" and #… URIs, so URI="foo" keeps its existing comment handling until this is resolved.
Problem
validateReferenceremoves a leading#from aReference'sURIonly when one is present, so a URI without one goes through the same ID lookup:xml-crypto/src/signed-xml.ts
Lines 537 to 566 in b40b4d6
As a result,
<Reference URI="foo">verifies against<item Id="foo">in the same document, exactly asURI="#foo"would. It has worked this way since the first implementation.validateElementAgainstReferencesdoes the same (L515). xml-crypto never writes such a URI when signing, and no fixture uses one.XMLDSig 1.1 §4.4.3.2 defines a same-document reference as an empty URI or
#followed by a fragment. Any other URI identifies a separate resource, and dereferencing it must produce an octet stream.URI="foo"is a relative URI for a resource namedfoo, not an element of the document being verified.The element selected is the one
#foowould select, so this is a conformance and interoperability problem, not a known bypass. A signature withURI="foo"that xml-crypto accepts would not verify in .NET or signxml.Other implementations
Reference.CalculateHashValuehandles a missing URI,""and#…, and throwsUriNotResolvedfor anything else: Reference.cs#L384-L468ResolverFragment, accepts only""and#…that isn't#xpointer(. Other URIs go to its other resolvers: ResolverFragment.java#L112-L126"",#xpointer(and#…. Any other URI needs a caller-supplied resolver and raisesInvalidInputwithout one. It also rejects aReferencewith noURI: processor.py#L153-L176#as local (file://) or remote and dereferences it externally, only if that URI type is enabled: transforms.c#L247-L260, transforms.c#L893-L913URI[1:]with the ID, dropping the first character whatever it is. It is looser than xml-crypto: validate.go#L310, validate.go#L488#handling, then requires the referenced element to be the signature's parent, so the loose form gains nothing there: xml.ts#L117-L139Related: a
Referencewithout aURIloadReferenceturns a missingURIattribute intoundefined, andaddReferencedefaults that to"", so the reference is verified as the whole document (L786-L797, L817). §4.4.3.1 says that whenURIis omitted, the receiving application is expected to know the identity of the object. signxml rejects such a reference, and .NET passes no input to the transform chain. The same change should decide this case.Proposal
Make reference validation fail with an explicit error when
URIis neither empty nor starts with#, and decide how to handle a missingURI. This rejects documents that verify today, so it belongs in 7.0.#589 already limits its comment removal to
""and#…URIs, soURI="foo"keeps its existing comment handling until this is resolved.