This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch 4.1.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/4.1.x-fixes by this push:
new 426ab88e9b6 Prevent XML Signature wrapping in AbstractXmlSigInHandler
(#3504)
426ab88e9b6 is described below
commit 426ab88e9b6776c128594b160d62831543841f7b
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Fri Sep 25 16:19:37 2026 +0100
Prevent XML Signature wrapping in AbstractXmlSigInHandler (#3504)
* Prevent XML Signature wrapping in AbstractXmlSigInHandler
* Apply suggestion from @reta
Co-authored-by: Andriy Redko <[email protected]>
* Fixup
---------
Co-authored-by: Andriy Redko <[email protected]>
(cherry picked from commit e43776be4dca84494478e45977f78b44546b7dd6)
---
.../rs/security/saml/SamlEnvelopedInHandler.java | 18 +-
.../rs/security/xml/AbstractXmlSigInHandler.java | 73 ++++--
.../cxf/rs/security/xml/XmlSigInHandlerTest.java | 266 +++++++++++++++++++++
3 files changed, 332 insertions(+), 25 deletions(-)
diff --git
a/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/saml/SamlEnvelopedInHandler.java
b/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/saml/SamlEnvelopedInHandler.java
index e22474e2ef8..79adb9bf29e 100644
---
a/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/saml/SamlEnvelopedInHandler.java
+++
b/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/saml/SamlEnvelopedInHandler.java
@@ -27,6 +27,7 @@ import javax.xml.stream.XMLStreamReader;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
+import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import jakarta.ws.rs.HttpMethod;
@@ -57,6 +58,7 @@ public class SamlEnvelopedInHandler extends
AbstractSamlInHandler {
}
Document doc = null;
+ Element signedBody = null;
InputStream is = message.getContent(InputStream.class);
if (is != null) {
try {
@@ -67,7 +69,15 @@ public class SamlEnvelopedInHandler extends
AbstractSamlInHandler {
} else {
XMLStreamReader reader = message.getContent(XMLStreamReader.class);
if (reader instanceof W3CDOMStreamReader) {
- doc = ((W3CDOMStreamReader)reader).getDocument();
+ W3CDOMStreamReader w3cReader = (W3CDOMStreamReader)reader;
+ doc = w3cReader.getDocument();
+
+ // A detached XML Signature has already been validated, and
the reader
+ // is restricted to the signed element, which is then the body
+ Node node = w3cReader.getCurrentNode();
+ if (node instanceof Element && node !=
doc.getDocumentElement()) {
+ signedBody = (Element)node;
+ }
}
}
if (doc == null) {
@@ -85,7 +95,11 @@ public class SamlEnvelopedInHandler extends
AbstractSamlInHandler {
validateToken(message, samlElement);
doc.getDocumentElement().removeChild(samlElement);
- if (bodyIsRoot) {
+ if (signedBody != null) {
+ message.setContent(XMLStreamReader.class,
+ new W3CDOMStreamReader(signedBody));
+ message.setContent(InputStream.class, null);
+ } else if (bodyIsRoot) {
message.setContent(XMLStreamReader.class,
new W3CDOMStreamReader(doc));
message.setContent(InputStream.class, null);
diff --git
a/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/xml/AbstractXmlSigInHandler.java
b/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/xml/AbstractXmlSigInHandler.java
index daa1c2a9d2e..585b3e099a0 100644
---
a/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/xml/AbstractXmlSigInHandler.java
+++
b/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/xml/AbstractXmlSigInHandler.java
@@ -25,6 +25,7 @@ import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
+import java.util.Set;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
@@ -55,6 +56,15 @@ import org.apache.xml.security.utils.Constants;
public class AbstractXmlSigInHandler extends AbstractXmlSecInHandler {
+ private static final Set<String> ALLOWED_TRANSFORMS = Set.of(
+ Transforms.TRANSFORM_ENVELOPED_SIGNATURE,
+ Transforms.TRANSFORM_C14N_OMIT_COMMENTS,
+ Transforms.TRANSFORM_C14N_WITH_COMMENTS,
+ Transforms.TRANSFORM_C14N11_OMIT_COMMENTS,
+ Transforms.TRANSFORM_C14N11_WITH_COMMENTS,
+ Transforms.TRANSFORM_C14N_EXCL_OMIT_COMMENTS,
+ Transforms.TRANSFORM_C14N_EXCL_WITH_COMMENTS);
+
private boolean removeSignature = true;
private boolean persistSignature = true;
private boolean keyInfoMustBeAvailable = true;
@@ -169,20 +179,23 @@ public class AbstractXmlSigInHandler extends
AbstractXmlSecInHandler {
if (!valid) {
throwFault("Signature validation failed", null);
}
+ Element signedEl = getSignedElement(root, ref);
+ // Only pass on the signed element. This is the root for an enveloped
signature, and
+ // a child of the (unsigned) root for a detached signature. The
reader's document
+ // is still the full document, e.g. for SamlEnvelopedInHandler to get
the assertion
+ Element body = isEnveloping(root) ? root : signedEl;
if (removeSignature) {
if (!isEnveloping(root)) {
- Element signedEl = getSignedElement(root, ref);
signedEl.removeAttribute("ID");
root.removeChild(signatureElement);
} else {
- Element actualBody = getActualBody(root);
Document newDoc = DOMUtils.createDocument();
- newDoc.adoptNode(actualBody);
- root = actualBody;
+ newDoc.adoptNode(signedEl);
+ body = signedEl;
}
}
message.setContent(XMLStreamReader.class,
- new W3CDOMStreamReader(root));
+ new W3CDOMStreamReader(body));
message.setContent(InputStream.class, null);
}
@@ -196,19 +209,6 @@ public class AbstractXmlSigInHandler extends
AbstractXmlSecInHandler {
}
- private Element getActualBody(Element envelopingSigElement) {
- Element objectNode = getNode(envelopingSigElement,
Constants.SignatureSpecNS, "Object", 0);
- if (objectNode == null) {
- throwFault("Object envelope is not available", null);
- }
- Element node = DOMUtils.getFirstElement(objectNode);
- if (node == null) {
- throwFault("No signed data is found", null);
- }
- return node;
-
- }
-
private Element getSignatureElement(Element sigParentElement) {
if (isEnveloping(sigParentElement)) {
return sigParentElement;
@@ -262,20 +262,36 @@ public class AbstractXmlSigInHandler extends
AbstractXmlSecInHandler {
String c14TransformExpected = sigProps != null ?
sigProps.getSignatureC14nTransform() : null;
boolean envelopedConfirmed = false;
for (int i = 0; i < transforms.getLength(); i++) {
+ String transformURI = null;
try {
Transform tr = transforms.item(i);
- if
(Transforms.TRANSFORM_ENVELOPED_SIGNATURE.equals(tr.getURI())) {
- envelopedConfirmed = true;
- } else if (c14TransformExpected != null &&
c14TransformExpected.equals(tr.getURI())) {
- c14TransformConfirmed = true;
- }
+ transformURI = tr.getURI();
} catch (Exception ex) {
throwFault("Problem accessing Transform instance", ex);
}
+ // Only allow transforms which cover the whole of the signed
element, so that
+ // no unsigned content (e.g. excluded via XPath) is passed on to
the application
+ if (!ALLOWED_TRANSFORMS.contains(transformURI)) {
+ throwFault("Signature Transform is not supported", null);
+ }
+ if (Transforms.TRANSFORM_ENVELOPED_SIGNATURE.equals(transformURI))
{
+ envelopedConfirmed = true;
+ } else if (c14TransformExpected != null &&
c14TransformExpected.equals(transformURI)) {
+ c14TransformConfirmed = true;
+ }
}
if (enveloped && !envelopedConfirmed) {
throwFault("Only enveloped signatures are currently supported",
null);
}
+ // The Signature is a child of the document root, so an enveloped
signature
+ // must reference the root. Otherwise the signed element could be
wrapped in
+ // unsigned content which would then be passed on to the application.
+ if (envelopedConfirmed && !enveloped) {
+ throwFault("Enveloped signature must reference the document root",
null);
+ }
+ if (isEnveloping(root)) {
+ validateEnvelopingReference(root, signedEl);
+ }
if (c14TransformExpected != null && !c14TransformConfirmed) {
throwFault("Transform Canonicalization is not supported", null);
}
@@ -291,6 +307,17 @@ public class AbstractXmlSigInHandler extends
AbstractXmlSecInHandler {
return signedEl;
}
+ // The signed element must be the only element in the only Object of the
enveloping
+ // Signature, so that no unsigned content can be passed on to the
application
+ private void validateEnvelopingReference(Element root, Element signedEl) {
+ List<Element> objects = DOMUtils.getChildrenWithName(root,
Constants.SignatureSpecNS, "Object");
+ if (objects.size() != 1 || signedEl.getParentNode() != objects.get(0)
+ || DOMUtils.getFirstElement(objects.get(0)) != signedEl
+ || DOMUtils.getNextElement(signedEl) != null) {
+ throwFault("Enveloping signature must reference the signed Object
content", null);
+ }
+ }
+
private Element getSignedElement(Element root, Reference ref) {
String rootId = root.getAttribute("ID");
String expectedID = ref.getURI().substring(1);
diff --git
a/rt/rs/security/xml/src/test/java/org/apache/cxf/rs/security/xml/XmlSigInHandlerTest.java
b/rt/rs/security/xml/src/test/java/org/apache/cxf/rs/security/xml/XmlSigInHandlerTest.java
new file mode 100644
index 00000000000..d5ca578c25a
--- /dev/null
+++
b/rt/rs/security/xml/src/test/java/org/apache/cxf/rs/security/xml/XmlSigInHandlerTest.java
@@ -0,0 +1,266 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.cxf.rs.security.xml;
+
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.Properties;
+
+import javax.xml.stream.XMLStreamReader;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import jakarta.ws.rs.WebApplicationException;
+import org.apache.cxf.helpers.DOMUtils;
+import org.apache.cxf.message.ExchangeImpl;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.message.MessageImpl;
+import org.apache.cxf.rt.security.SecurityConstants;
+import org.apache.cxf.staxutils.StaxUtils;
+import org.apache.cxf.staxutils.W3CDOMStreamReader;
+import org.apache.wss4j.common.crypto.Crypto;
+import org.apache.wss4j.common.crypto.CryptoFactory;
+import org.apache.wss4j.common.crypto.CryptoType;
+import org.apache.xml.security.algorithms.MessageDigestAlgorithm;
+import org.apache.xml.security.signature.XMLSignature;
+import org.apache.xml.security.transforms.Transforms;
+import org.apache.xml.security.utils.Constants;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+public class XmlSigInHandlerTest {
+
+ private static Crypto crypto;
+
+ @BeforeClass
+ public static void setUpCrypto() throws Exception {
+ org.apache.xml.security.Init.init();
+ Properties props = new Properties();
+ props.put("org.apache.wss4j.crypto.provider",
"org.apache.wss4j.common.crypto.Merlin");
+ props.put("org.apache.wss4j.crypto.merlin.keystore.type", "jks");
+ props.put("org.apache.wss4j.crypto.merlin.keystore.password",
"password");
+ props.put("org.apache.wss4j.crypto.merlin.keystore.alias", "alice");
+ props.put("org.apache.wss4j.crypto.merlin.keystore.file", "alice.jks");
+ crypto = CryptoFactory.getInstance(props);
+ }
+
+ @Test
+ public void testEnvelopedSignature() throws Exception {
+ Document doc = createSignedDocument();
+
+ assertBody(doc, "CXF");
+ }
+
+ @Test
+ public void testWrappedEnvelopedSignatureIsRejected() throws Exception {
+ Document signedDoc = createSignedDocument();
+ Element signedRoot = signedDoc.getDocumentElement();
+ Element signature =
+ DOMUtils.getFirstChildWithName(signedRoot,
Constants.SignatureSpecNS, "Signature");
+
+ // Wrap the signed element in a new unsigned root and move the
Signature
+ // to be a direct child of that root
+ Document doc = DOMUtils.createDocument();
+ Element root = createBook(doc, "evil");
+ doc.appendChild(root);
+ signedRoot.removeChild(signature);
+ root.appendChild(doc.importNode(signedRoot, true));
+ root.appendChild(doc.importNode(signature, true));
+
+ assertRejected(doc);
+ }
+
+ @Test
+ public void testEnvelopingSignature() throws Exception {
+ Document doc = createEnvelopingSignedDocument();
+
+ assertBody(doc, "CXF");
+ }
+
+ @Test
+ public void testEnvelopingSignatureWithUnsignedObjectContentIsRejected()
throws Exception {
+ Document doc = createEnvelopingSignedDocument();
+ Element object =
+ DOMUtils.getFirstChildWithName(doc.getDocumentElement(),
Constants.SignatureSpecNS, "Object");
+
+ // Add an unsigned element before the signed element in the same Object
+ object.insertBefore(createBook(doc, "evil"), object.getFirstChild());
+
+ assertRejected(doc);
+ }
+
+ @Test
+ public void testEnvelopingSignatureWithUnsignedObjectIsRejected() throws
Exception {
+ Document doc = createEnvelopingSignedDocument();
+ Element root = doc.getDocumentElement();
+ Element object = DOMUtils.getFirstChildWithName(root,
Constants.SignatureSpecNS, "Object");
+
+ // Add an unsigned Object before the signed Object
+ Element evilObject = doc.createElementNS(Constants.SignatureSpecNS,
"ds:Object");
+ evilObject.appendChild(createBook(doc, "evil"));
+ root.insertBefore(evilObject, object);
+
+ assertRejected(doc);
+ }
+
+ @Test
+ public void testEnvelopingSignatureWithObjectInKeyInfo() throws Exception {
+ Document doc = createEnvelopingSignedDocument();
+ Element keyInfo =
+ DOMUtils.getFirstChildWithName(doc.getDocumentElement(),
Constants.SignatureSpecNS, "KeyInfo");
+
+ // An unsigned Object nested in the KeyInfo must not be passed on as
the body
+ Element evilObject = doc.createElementNS(Constants.SignatureSpecNS,
"ds:Object");
+ evilObject.appendChild(createBook(doc, "evil"));
+ keyInfo.appendChild(evilObject);
+
+ assertBody(doc, "CXF");
+ }
+
+ @Test
+ public void testDetachedSignature() throws Exception {
+ Document doc = createDetachedSignedDocument();
+
+ // Only the signed element is passed on, not the unsigned wrapper
+ assertBody(doc, "CXF");
+ }
+
+ @Test
+ public void testDetachedSignatureWithUnsignedContent() throws Exception {
+ Document doc = createDetachedSignedDocument();
+
+ // Add unsigned content to the unsigned root, before the signed element
+ Element root = doc.getDocumentElement();
+ root.insertBefore(createBook(doc, "evil"), root.getFirstChild());
+
+ assertBody(doc, "CXF");
+ }
+
+ private static void assertBody(Document doc, String expectedName) throws
Exception {
+ Message message = createMessage(doc);
+ new AbstractXmlSigInHandler() { }.checkSignature(message);
+
+ XMLStreamReader reader = message.getContent(XMLStreamReader.class);
+ assertNotNull(reader);
+ Element root = StaxUtils.read(reader).getDocumentElement();
+ assertEquals("Book", root.getLocalName());
+ assertEquals(expectedName,
DOMUtils.getFirstElement(root).getTextContent());
+ }
+
+ private static void assertRejected(Document doc) {
+ Message message = createMessage(doc);
+ try {
+ new AbstractXmlSigInHandler() { }.checkSignature(message);
+ fail("Failure expected on a wrapped signature");
+ } catch (WebApplicationException ex) {
+ assertEquals(400, ex.getResponse().getStatus());
+ }
+ }
+
+ private static Message createMessage(Document doc) {
+ Message message = new MessageImpl();
+ message.setExchange(new ExchangeImpl());
+ message.put(Message.HTTP_REQUEST_METHOD, "POST");
+ message.put(SecurityConstants.SIGNATURE_CRYPTO, crypto);
+ message.setContent(XMLStreamReader.class, new
W3CDOMStreamReader(doc.getDocumentElement()));
+ return message;
+ }
+
+ private static Document createSignedDocument() throws Exception {
+ Document doc = DOMUtils.createDocument();
+ Element root = createBook(doc, "CXF");
+ doc.appendChild(root);
+
+ String id = "_book";
+ root.setAttributeNS(null, "Id", id);
+ root.setIdAttributeNS(null, "Id", true);
+
+ XMLSignature sig = new XMLSignature(doc, "",
XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256);
+ root.appendChild(sig.getElement());
+ Transforms transforms = new Transforms(doc);
+ transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE);
+ transforms.addTransform(Transforms.TRANSFORM_C14N_EXCL_OMIT_COMMENTS);
+ sig.addDocument("#" + id, transforms,
MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256);
+ sign(sig);
+ return doc;
+ }
+
+ private static Document createEnvelopingSignedDocument() throws Exception {
+ Document doc = DOMUtils.createDocument();
+ Element book = createBook(doc, "CXF");
+
+ String id = "_book";
+ book.setAttributeNS(null, "Id", id);
+ book.setIdAttributeNS(null, "Id", true);
+
+ XMLSignature sig = new XMLSignature(doc, "",
XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256);
+ doc.appendChild(sig.getElement());
+ Element object = doc.createElementNS(Constants.SignatureSpecNS,
"ds:Object");
+ object.appendChild(book);
+ sig.getElement().appendChild(object);
+ Transforms transforms = new Transforms(doc);
+ transforms.addTransform(Transforms.TRANSFORM_C14N_EXCL_OMIT_COMMENTS);
+ sig.addDocument("#" + id, transforms,
MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256);
+ sign(sig);
+ return doc;
+ }
+
+ private static Document createDetachedSignedDocument() throws Exception {
+ Document doc = DOMUtils.createDocument();
+ Element root = doc.createElementNS("http://org.apache.cxf/rs/env",
"env:Envelope");
+ doc.appendChild(root);
+ Element book = createBook(doc, "CXF");
+ root.appendChild(book);
+
+ String id = "_book";
+ book.setAttributeNS(null, "Id", id);
+ book.setIdAttributeNS(null, "Id", true);
+
+ XMLSignature sig = new XMLSignature(doc, "",
XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256);
+ root.appendChild(sig.getElement());
+ Transforms transforms = new Transforms(doc);
+ transforms.addTransform(Transforms.TRANSFORM_C14N_EXCL_OMIT_COMMENTS);
+ sig.addDocument("#" + id, transforms,
MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256);
+ sign(sig);
+ return doc;
+ }
+
+ private static Element createBook(Document doc, String bookName) {
+ Element book = doc.createElementNS(null, "Book");
+ Element name = doc.createElementNS(null, "name");
+ name.setTextContent(bookName);
+ book.appendChild(name);
+ return book;
+ }
+
+ private static void sign(XMLSignature sig) throws Exception {
+ CryptoType cryptoType = new CryptoType(CryptoType.TYPE.ALIAS);
+ cryptoType.setAlias("alice");
+ X509Certificate cert = crypto.getX509Certificates(cryptoType)[0];
+ PrivateKey key = crypto.getPrivateKey("alice", "password");
+ sig.addKeyInfo(cert);
+ sig.sign(key);
+ }
+}