Skip to content

Reference URIs without "#" are resolved as same-document IDs #594

Description

@cjbarth

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions