Repository navigation
Description
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:
Lines 537 to 566 in b40b4d6
| 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.CalculateHashValuehandles a missing URI,""and#…, and throwsUriNotResolvedfor 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 raisesInvalidInputwithout one. It also rejects aReferencewith noURI: 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.
Activity
Require a same-document reference in getVerifiedXml
Cite the spec, not the dependency's issue tracker