Skip to content

Commit 973814c

Browse files
committed
Several security improvements:
- Conditions element required and unique. - AuthnStatement element required and unique. - SPNameQualifier must math the SP EntityID - Reject saml:Attribute element with same “Name” attribute - Reject empty nameID - Require Issuer element. (Must match IdP EntityID). - Destination value can't be blank (if present must match ACS URL). - Check that the EncryptedAssertion element only contains 1 Assertion element.
1 parent 3fcda2c commit 973814c

19 files changed

Lines changed: 357 additions & 44 deletions

core/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
<dependency>
4949
<groupId>joda-time</groupId>
5050
<artifactId>joda-time</artifactId>
51-
<version>2.8.2</version>
51+
<version>2.9.4</version>
5252
</dependency>
5353

5454
<!-- commons -->

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

Lines changed: 91 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -199,22 +199,34 @@ public boolean isValid(String requestId) {
199199
}
200200
}
201201

202+
// Validate Conditions element exists
203+
if (!this.checkOneCondition()) {
204+
throw new Exception("The Assertion must include a Conditions element");
205+
}
206+
202207
// Validate Assertion timestamps
203208
if (!this.validateTimestamps()) {
204209
throw new Exception("Timing issues (please check your clock settings)");
205210
}
206211

212+
// Validate AuthnStatement element exists and is unique
213+
if (!this.checkOneAuthnStatement()) {
214+
throw new Exception("The Assertion must include an AuthnStatement element");
215+
}
216+
207217
// EncryptedAttributes are not supported
208218
NodeList encryptedAttributeNodes = this.queryAssertion("/saml:AttributeStatement/saml:EncryptedAttribute");
209219
if (encryptedAttributeNodes.getLength() > 0) {
210220
throw new Exception("There is an EncryptedAttribute in the Response and this SP not support them");
211221
}
212-
222+
213223
// Check destination
214224
if (rootElement.hasAttribute("Destination")) {
215225
String destinationUrl = rootElement.getAttribute("Destination");
216226
if (destinationUrl != null) {
217-
if (!destinationUrl.isEmpty() && !destinationUrl.equals(currentUrl)) {
227+
if (destinationUrl.isEmpty()) {
228+
throw new Exception("The response has an empty Destination value");
229+
} else if (!destinationUrl.equals(currentUrl)) {
218230
throw new Exception("The response was received at " + currentUrl + " instead of "
219231
+ destinationUrl);
220232
}
@@ -317,7 +329,6 @@ private void validateSubjectConfirmation(String responseInResponseTo) throws Exc
317329
continue;
318330
}
319331

320-
321332
Node notOnOrAfter = subjectConfirmationDataNodes.item(c).getAttributes().getNamedItem("NotOnOrAfter");
322333
if (notOnOrAfter == null) {
323334
validationIssues.add(new SubjectConfirmationIssue(i, "SubjectConfirmationData doesn't contain a NotOnOrAfter attribute"));
@@ -370,7 +381,7 @@ public HashMap<String,String> getNameIdData() throws Exception {
370381
NodeList encryptedIDNodes = this.queryAssertion("/saml:Subject/saml:EncryptedID/xenc:EncryptedData");
371382
NodeList nameIdNodes;
372383
Element nameIdElem;
373-
if (encryptedIDNodes.getLength() > 0) {
384+
if (encryptedIDNodes.getLength() == 1) {
374385
Element encryptedData = (Element) encryptedIDNodes.item(0);
375386
PrivateKey key = settings.getSPkey();
376387
if (key == null) {
@@ -387,17 +398,28 @@ public HashMap<String,String> getNameIdData() throws Exception {
387398
nameIdNodes = this.queryAssertion("/saml:Subject/saml:NameID");
388399
}
389400

390-
if (nameIdNodes != null && nameIdNodes.getLength() > 0) {
401+
if (nameIdNodes != null && nameIdNodes.getLength() == 1) {
391402
nameIdElem = (Element) nameIdNodes.item(0);
392403

393404
if (nameIdElem != null) {
394-
nameIdData.put("Value", nameIdElem.getTextContent());
405+
String value = nameIdElem.getTextContent();
406+
if (value.isEmpty()) {
407+
throw new Exception("An empty NameID value found");
408+
}
409+
410+
nameIdData.put("Value", value);
395411

396412
if (nameIdElem.hasAttribute("Format")) {
397413
nameIdData.put("Format", nameIdElem.getAttribute("Format"));
398414
}
399415
if (nameIdElem.hasAttribute("SPNameQualifier")) {
400-
nameIdData.put("SPNameQualifier", nameIdElem.getAttribute("SPNameQualifier"));
416+
String spId = settings.getSpEntityId();
417+
String spNameQualifier = nameIdElem.getAttribute("SPNameQualifier");
418+
if (!spNameQualifier.equals(settings.getSpEntityId())) {
419+
throw new Exception("The SPNameQualifier value mistmatch the SP entityID value.");
420+
} else {
421+
nameIdData.put("SPNameQualifier", spNameQualifier);
422+
}
401423
}
402424
if (nameIdElem.hasAttribute("NameQualifier")) {
403425
nameIdData.put("NameQualifier", nameIdElem.getAttribute("NameQualifier"));
@@ -433,17 +455,24 @@ public String getNameId() throws Exception {
433455
*
434456
* @return the attributes of the SAML Assertion
435457
*
436-
* @throws XPathExpressionException
458+
* @throws Exception
437459
*/
438-
public HashMap<String, List<String>> getAttributes() throws XPathExpressionException {
460+
public HashMap<String, List<String>> getAttributes() throws Exception {
439461
HashMap<String, List<String>> attributes = new HashMap<String, List<String>>();
440462

441463
NodeList nodes = this.queryAssertion("/saml:AttributeStatement/saml:Attribute");
442-
464+
List<String> processedNames = new ArrayList<String>();
465+
443466
if (nodes.getLength() != 0) {
444467
for (int i = 0; i < nodes.getLength(); i++) {
445468
NamedNodeMap attrName = nodes.item(i).getAttributes();
446469
String attName = attrName.getNamedItem("Name").getNodeValue();
470+
if (processedNames.contains(attName)) {
471+
throw new Exception("Found an Attribute element with duplicated Name");
472+
} else {
473+
processedNames.add(attName);
474+
}
475+
447476
NodeList childrens = nodes.item(i).getChildNodes();
448477

449478
List<String> attrValues = new ArrayList<String>();
@@ -495,14 +524,14 @@ public static SamlResponseStatus getStatus(Document dom) throws IllegalArgumentE
495524
String statusExpr = "/samlp:Response/samlp:Status";
496525

497526
NodeList statusEntry = Util.query(dom, statusExpr, null);
498-
if (statusEntry.getLength() == 0) {
527+
if (statusEntry.getLength() != 1) {
499528
throw new IllegalArgumentException("Missing Status on response");
500529
}
501530
NodeList codeEntry;
502531

503532
codeEntry = Util.query(dom, statusExpr + "/samlp:StatusCode", (Element) statusEntry.item(0));
504533

505-
if (codeEntry.getLength() == 0) {
534+
if (codeEntry.getLength() != 1) {
506535
throw new IllegalArgumentException("Missing Status Code on response");
507536
}
508537

@@ -511,7 +540,7 @@ public static SamlResponseStatus getStatus(Document dom) throws IllegalArgumentE
511540

512541
NodeList messageEntry = Util.query(dom, statusExpr + "/samlp:StatusMessage",
513542
(Element) statusEntry.item(0));
514-
if (messageEntry.getLength() > 0) {
543+
if (messageEntry.getLength() == 1) {
515544
status.setStatusMessage(messageEntry.item(0).getTextContent());
516545
}
517546
return status;
@@ -521,6 +550,38 @@ public static SamlResponseStatus getStatus(Document dom) throws IllegalArgumentE
521550
}
522551
}
523552

553+
/**
554+
* Checks that the samlp:Response/saml:Assertion/saml:Conditions element exists and is unique.
555+
*
556+
* @return true if the Conditions element exists and is unique
557+
*
558+
* @throws XPathExpressionException
559+
*/
560+
public Boolean checkOneCondition() throws XPathExpressionException {
561+
NodeList entries = this.queryAssertion("/saml:Conditions");
562+
if (entries.getLength() == 1) {
563+
return true;
564+
} else {
565+
return false;
566+
}
567+
}
568+
569+
/**
570+
* Checks that the samlp:Response/saml:Assertion/saml:AuthnStatement element exists and is unique.
571+
*
572+
* @return true if the AuthnStatement element exists and is unique
573+
*
574+
* @throws XPathExpressionException
575+
*/
576+
public Boolean checkOneAuthnStatement() throws XPathExpressionException {
577+
NodeList entries = this.queryAssertion("/saml:AuthnStatement");
578+
if (entries.getLength() == 1) {
579+
return true;
580+
} else {
581+
return false;
582+
}
583+
}
584+
524585
/**
525586
* Gets the audiences.
526587
*
@@ -548,10 +609,9 @@ public List<String> getAudiences() throws XPathExpressionException {
548609
* Gets the Issuers (from Response and Assertion).
549610
*
550611
* @return the issuers of the assertion/response
551-
*
552-
* @throws XPathExpressionException
612+
* @throws Exception
553613
*/
554-
public List<String> getIssuers() throws XPathExpressionException {
614+
public List<String> getIssuers() throws Exception {
555615
List<String> issuers = new ArrayList<String>();
556616
String value;
557617
NodeList responseIssuer = Util.query(samlResponseDocument, "/samlp:Response/saml:Issuer");
@@ -560,6 +620,8 @@ public List<String> getIssuers() throws XPathExpressionException {
560620
if (!issuers.contains(value)) {
561621
issuers.add(value);
562622
}
623+
} else {
624+
throw new Exception("Issuer of the Response not found.");
563625
}
564626

565627
NodeList assertionIssuer = this.queryAssertion("/saml:Issuer");
@@ -568,6 +630,8 @@ public List<String> getIssuers() throws XPathExpressionException {
568630
if (!issuers.contains(value)) {
569631
issuers.add(value);
570632
}
633+
} else {
634+
throw new Exception("Issuer of the Assertion not found.");
571635
}
572636

573637
return issuers;
@@ -616,7 +680,9 @@ public String getSessionIndex() throws XPathExpressionException {
616680
*
617681
*/
618682
public String getAssertionId() throws XPathExpressionException {
619-
validateNumAssertions();
683+
if (!validateNumAssertions()) {
684+
throw new IllegalArgumentException("SAML Response must contain 1 Assertion.");
685+
}
620686
final NodeList assertionNode = queryAssertion("");
621687
return assertionNode.item(0).getAttributes().getNamedItem("ID").getNodeValue();
622688
}
@@ -648,7 +714,13 @@ public Boolean validateNumAssertions() throws IllegalArgumentException {
648714
NodeList encryptedAssertionNodes = samlResponseDocument.getElementsByTagNameNS(Constants.NS_SAML, "EncryptedAssertion");
649715
NodeList assertionNodes = samlResponseDocument.getElementsByTagNameNS(Constants.NS_SAML, "Assertion");
650716

651-
return (assertionNodes.getLength() + encryptedAssertionNodes.getLength() == 1);
717+
Boolean valid = assertionNodes.getLength() + encryptedAssertionNodes.getLength() == 1;
718+
719+
if (encrypted) {
720+
valid = valid && decryptedDocument.getElementsByTagNameNS(Constants.NS_SAML, "Assertion").getLength() == 1;
721+
}
722+
723+
return valid;
652724
}
653725

654726
/**
@@ -835,7 +907,7 @@ private NodeList queryAssertion(String assertionXpath) throws XPathExpressionExc
835907
// let see if the whole response signed?
836908
String signedMessageQuery = "/samlp:Response/" + signatureExpr;
837909
nodeList = query(signedMessageQuery, null);
838-
if (nodeList.getLength() > 0) {
910+
if (nodeList.getLength() == 1) {
839911
Node responseReferenceNode = nodeList.item(0);
840912
String responseId = responseReferenceNode.getAttributes().getNamedItem("URI").getNodeValue();
841913
if (responseId != null && !responseId.isEmpty()) {

core/src/main/java/com/onelogin/saml2/logout/LogoutRequest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ public static Map<String, String> getNameIdData(Document samlLogoutRequestDocume
387387
NodeList nameIdNodes;
388388
Element nameIdElem;
389389

390-
if (encryptedIDNodes.getLength() > 0) {
390+
if (encryptedIDNodes.getLength() == 1) {
391391
if (key == null) {
392392
throw new IllegalArgumentException("Key is required in order to decrypt the NameID");
393393
}
@@ -396,15 +396,15 @@ public static Map<String, String> getNameIdData(Document samlLogoutRequestDocume
396396
Util.decryptElement(encryptedData, key);
397397
nameIdNodes = Util.query(samlLogoutRequestDocument, "/samlp:LogoutRequest/saml:NameID");
398398

399-
if (nameIdNodes == null || nameIdNodes.getLength() == 0) {
399+
if (nameIdNodes == null || nameIdNodes.getLength() != 1) {
400400
throw new Exception("Not able to decrypt the EncryptedID and get a NameID");
401401
}
402402
}
403403
else {
404404
nameIdNodes = Util.query(samlLogoutRequestDocument, "/samlp:LogoutRequest/saml:NameID");
405405
}
406406

407-
if (nameIdNodes != null && nameIdNodes.getLength() > 0) {
407+
if (nameIdNodes != null && nameIdNodes.getLength() == 1) {
408408
nameIdElem = (Element) nameIdNodes.item(0);
409409
} else {
410410
throw new Exception("No name id found in Logout Request.");

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -833,7 +833,7 @@ public static Boolean validateMetadataSign(Document doc, X509Certificate cert, S
833833

834834
if (signNodesToValidate.getLength() > 0) {
835835
for (int i = 0; i < signNodesToValidate.getLength(); i++) {
836-
Node signNode = signNodesToValidate.item(0);
836+
Node signNode = signNodesToValidate.item(i);
837837
if (!validateSignNode(signNode, cert, fingerprint, alg)) {
838838
return false;
839839
}

0 commit comments

Comments
 (0)