Skip to content

Commit 16a1d51

Browse files
committed
fixes #78: make sure the correct Signature elements are verified
- checks that the found Signature elements match their expected schema locations, and that there's only one of each - make sure the expected Signatures are always verified - a few other small hardenings
1 parent cc296ae commit 16a1d51

5 files changed

Lines changed: 172 additions & 83 deletions

File tree

core/src/main/java/com/onelogin/saml2/authn/SamlResponse.java

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ public boolean isValid(String requestId) {
271271
}
272272
}
273273

274-
if (!Util.validateSign(documentToValidate, cert, fingerprint, alg)) {
274+
if (!Util.validateSign(documentToValidate, cert, fingerprint, alg, settings.getWantMessagesSigned(), settings.getWantAssertionsSigned())) {
275275
throw new Exception("Signature validation failed. SAML Response rejected");
276276
}
277277
}
@@ -676,6 +676,22 @@ public ArrayList<String> processSignedElements() throws Exception {
676676
if (!signedElement.equals("Response") && !signedElement.equals("Assertion")) {
677677
throw new Exception("Invalid Signature Element " + signedElement + " SAML Response rejected");
678678
}
679+
680+
// check that the signed elements found here, are the ones that will be verified
681+
// by com.onelogin.saml2.util.Util.validateSign()
682+
if (signedElement.equals("Response")) {
683+
final NodeList expectedSignatureNode = query(Util.RESPONSE_SIGNATURE_XPATH, null);
684+
if (expectedSignatureNode.getLength() != 1 || signedElements.contains(signedElement)) {
685+
throw new Exception("Unexpected number of Response signatures found. SAML Response rejected.");
686+
}
687+
}
688+
689+
if (signedElement.equals("Assertion")) {
690+
final NodeList expectedSignatureNode = query(Util.ASSERTION_SIGNATURE_XPATH, null);
691+
if (expectedSignatureNode.getLength() != 1 || signedElements.contains(signedElement)) {
692+
throw new Exception("Unexpected number of Response signatures found. SAML Response rejected.");
693+
}
694+
}
679695

680696
// Check that reference URI matches the parent ID and no duplicate References or IDs
681697
Node idNode = signNode.getParentNode().getAttributes().getNamedItem("ID");
@@ -689,8 +705,8 @@ public ArrayList<String> processSignedElements() throws Exception {
689705
}
690706
verifiedIds.add(idValue);
691707

692-
NodeList refNodes = Util.query(null, ".//ds:Reference", signNode);
693-
if (refNodes.getLength() > 0) {
708+
NodeList refNodes = Util.query(null, "ds:SignedInfo/ds:Reference", signNode);
709+
if (refNodes.getLength() == 1) {
694710
Node refNode = refNodes.item(0);
695711
Node seiNode = refNode.getAttributes().getNamedItem("URI");
696712
if (seiNode != null && seiNode.getNodeValue() != null && !seiNode.getNodeValue().isEmpty()) {
@@ -704,6 +720,10 @@ public ArrayList<String> processSignedElements() throws Exception {
704720
}
705721
verifiedSeis.add(sei);
706722
}
723+
} else {
724+
// Signatures MUST contain a single <ds:Reference> containing a same-document reference to the ID
725+
// attribute value of the root element of the assertion or protocol message being signed
726+
throw new Exception("Unexpected number of Reference nodes found for signature. SAML Response rejected.");
707727
}
708728

709729
signedElements.add(signedElement);
@@ -809,13 +829,10 @@ public String getError() {
809829
*
810830
*/
811831
private NodeList queryAssertion(String assertionXpath) throws XPathExpressionException {
812-
String assertionExpr;
813-
814-
assertionExpr = "/saml:Assertion";
815-
816-
String signatureExpr = "ds:Signature/ds:SignedInfo/ds:Reference";
832+
final String assertionExpr = "/saml:Assertion";
833+
final String signatureExpr = "ds:Signature/ds:SignedInfo/ds:Reference";
817834

818-
String nameQuery = "";
835+
String nameQuery;
819836
String signedAssertionQuery = "/samlp:Response" + assertionExpr + "/" + signatureExpr;
820837
NodeList nodeList = query(signedAssertionQuery, null);
821838
if (nodeList.getLength() == 0 ) {
@@ -835,19 +852,6 @@ private NodeList queryAssertion(String assertionXpath) throws XPathExpressionExc
835852
// On this case there is no element signed, the query will work but
836853
// the response validation will throw and error.
837854
nameQuery = "/samlp:Response";
838-
839-
// If we want to change this behaviour, uncomment this block.
840-
// but test should be updated then.
841-
842-
// Trick in order to return empty NodeList (not instanciable)
843-
/*
844-
XPath xpath = XPathFactory.newInstance().newXPath();
845-
NodeList noresult = null;
846-
try {
847-
noresult = (NodeList) xpath.evaluate("/noresult", Util.convertStringToDocument("<null></null>"), XPathConstants.NODESET);
848-
} catch (ParserConfigurationException | SAXException | IOException e) {}
849-
return noresult;
850-
*/
851855
}
852856
nameQuery += assertionExpr;
853857
} else { // there is a signed assertion

core/src/main/java/com/onelogin/saml2/util/Util.java

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ public final class Util {
9696
private static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(DateTimeZone.UTC);
9797
private static final DateTimeFormatter DATE_TIME_FORMAT_MILLS = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(DateTimeZone.UTC);
9898
public static final String UNIQUE_ID_PREFIX = "ONELOGIN_";
99+
public static final String RESPONSE_SIGNATURE_XPATH = "/samlp:Response/ds:Signature";
100+
public static final String ASSERTION_SIGNATURE_XPATH = "/samlp:Response/saml:Assertion/ds:Signature";
101+
102+
private static final Logger log = LoggerFactory.getLogger(Util.class);
99103

100104
/**
101105
* This function load an XML string in a save way. Prevent XEE/XXE Attacks
@@ -220,9 +224,13 @@ public static boolean validateXML(Document xmlDocument, URL schemaUrl) throws Ex
220224
Source xmlSource = new DOMSource(xmlDocument);
221225
validator.validate(xmlSource);
222226

223-
return !errorAcumulator.hasError();
227+
final boolean isValid = !errorAcumulator.hasError();
228+
if (!isValid) {
229+
LOGGER.warn("Errors found when validating SAML response with schema: " + errorAcumulator.getErrorXML());
230+
}
231+
return isValid;
224232
} catch (Exception e) {
225-
LOGGER.debug("Error executing validateXML: " + e.getMessage(), e);
233+
LOGGER.warn("Error executing validateXML: " + e.getMessage(), e);
226234
return false;
227235
}
228236
}
@@ -785,25 +793,44 @@ public static String signatureAlgConversion(String sign) {
785793
* The fingerprint of the public certificate
786794
* @param alg
787795
* The signature algorithm method
796+
* @param wantAssertionSigned whether the caller requires assertions to be signed
797+
* @param wantResponseSigned whether the caller requires responses to be signed
788798
*
789-
* @return True if the sign is valid, false otherwise.
799+
* @return True if the required signatures are present and the present signatures are valid, false otherwise.
790800
*/
791-
public static Boolean validateSign(Document doc, X509Certificate cert, String fingerprint, String alg) {
792-
NodeList signNodesToValidate;
801+
public static Boolean validateSign(Document doc, X509Certificate cert, String fingerprint, String alg,
802+
boolean wantResponseSigned, boolean wantAssertionSigned) {
793803
try {
794-
signNodesToValidate = query(doc, "/samlp:Response/ds:Signature");
795-
if (signNodesToValidate.getLength() == 0) {
796-
signNodesToValidate = query(doc, "/samlp:Response/saml:Assertion/ds:Signature");
797-
}
804+
boolean validResponseSignature = checkSignature(doc, cert, fingerprint, alg, wantResponseSigned, RESPONSE_SIGNATURE_XPATH);
805+
boolean validAssertionSignature = checkSignature(doc, cert, fingerprint, alg, wantAssertionSigned, ASSERTION_SIGNATURE_XPATH);
798806

799-
if (signNodesToValidate.getLength() == 1) {
800-
return validateSignNode(signNodesToValidate.item(0), cert, fingerprint, alg);
801-
}
802-
} catch (XPathExpressionException e) {}
807+
return validResponseSignature && validAssertionSignature;
808+
} catch (XPathExpressionException e) {
809+
log.warn("Failed to find signature nodes", e);
810+
}
803811
return false;
804812
}
805813

806-
/**
814+
private static boolean checkSignature(Document doc, X509Certificate cert, String fingerprint, String alg, boolean required, final String xpath) throws XPathExpressionException {
815+
final NodeList responseSignature = query(doc, xpath);
816+
final int signatureCount = responseSignature.getLength();
817+
if (signatureCount > 1) {
818+
log.warn("Unexpected number of signatures found matching " + xpath + ": " + signatureCount);
819+
return false;
820+
}
821+
822+
if (signatureCount == 0) {
823+
if (required) {
824+
log.warn("No signature matching " + xpath + ", found but was required");
825+
return false;
826+
}
827+
return true;
828+
}
829+
830+
return validateSignNode(responseSignature.item(0), cert, fingerprint, alg);
831+
}
832+
833+
/**
807834
* Validate signature (Metadata).
808835
*
809836
* @param doc

core/src/test/java/com/onelogin/saml2/test/authn/AuthnResponseTest.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,23 @@ public void testDoesNotAllowSignatureWrappingAttack4() throws Exception {
609609
assertEquals("someone@example.org", samlResponse.getNameId());
610610
}
611611

612+
@Test
613+
public void testValidatesTheExpectedSignatures() throws Exception {
614+
// having
615+
Saml2Settings settings = new SettingsBuilder().fromFile("config/config.my.properties").build();
616+
settings.setWantAssertionsSigned(true);
617+
settings.setWantMessagesSigned(true);
618+
619+
String samlResponseEncoded = Util.base64encoder(Util.getFileAsString("data/responses/invalids/attacks/response_with_spoofed_response_signature.xml"));
620+
621+
// when
622+
SamlResponse samlResponse = new SamlResponse(settings, newHttpRequest(samlResponseEncoded));
623+
624+
// then
625+
assertFalse(samlResponse.isValid());
626+
assertEquals("Unexpected number of Response signatures found. SAML Response rejected.", samlResponse.getError());
627+
}
628+
612629
/**
613630
* Tests the getSessionNotOnOrAfter method of SamlResponse
614631
*
@@ -851,6 +868,7 @@ public void testIsValidWrongEncryptedID() throws Exception {
851868
Saml2Settings settings = new SettingsBuilder().fromFile("config/config.my.properties").build();
852869
String samlResponseEncoded = Util.getFileAsString("data/responses/invalids/response_encrypted_subconfirm_as_nameid.xml.base64");
853870
settings.setStrict(false);
871+
settings.setWantAssertionsSigned(false);
854872
SamlResponse samlResponse = new SamlResponse(settings, newHttpRequest(samlResponseEncoded));
855873
assertTrue(samlResponse.isValid());
856874
String nameId = samlResponse.getNameId();
@@ -1630,17 +1648,17 @@ public void testIsInValidSign() throws Exception {
16301648
samlResponseEncoded = Util.getFileAsString("data/responses/invalids/triple_signed_response.xml.base64");
16311649
samlResponse = new SamlResponse(settings, newHttpRequest(samlResponseEncoded));
16321650
assertFalse(samlResponse.isValid());
1633-
assertEquals("Duplicated ID. SAML Response rejected", samlResponse.getError());
1651+
assertEquals("Unexpected number of Response signatures found. SAML Response rejected.", samlResponse.getError());
16341652

16351653
samlResponseEncoded = Util.getFileAsString("data/responses/invalids/signed_assertion_response_with_2signatures.xml.base64");
16361654
samlResponse = new SamlResponse(settings, newHttpRequest(samlResponseEncoded));
16371655
assertFalse(samlResponse.isValid());
1638-
assertEquals("Duplicated ID. SAML Response rejected", samlResponse.getError());
1656+
assertEquals("Unexpected number of Response signatures found. SAML Response rejected.", samlResponse.getError());
16391657

16401658
samlResponseEncoded = Util.getFileAsString("data/responses/invalids/signed_message_response_with_2signatures.xml.base64");
16411659
samlResponse = new SamlResponse(settings, newHttpRequest(samlResponseEncoded));
16421660
assertFalse(samlResponse.isValid());
1643-
assertEquals("Duplicated ID. SAML Response rejected", samlResponse.getError());
1661+
assertEquals("Unexpected number of Response signatures found. SAML Response rejected.", samlResponse.getError());
16441662

16451663
samlResponseEncoded = Util.getFileAsString("data/responses/invalids/wrong_signed_element.xml.base64");
16461664
samlResponse = new SamlResponse(settings, newHttpRequest(samlResponseEncoded));

0 commit comments

Comments
 (0)