Skip to content

Commit 9b8e8e2

Browse files
committed
Merge master brunch
2 parents c3e33e7 + 3a9d1fe commit 9b8e8e2

16 files changed

+294
-42
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Ruby SAML [![Build Status](https://secure.travis-ci.org/onelogin/ruby-saml.png)](http://travis-ci.org/onelogin/ruby-saml) [![Coverage Status](https://coveralls.io/repos/onelogin/ruby-saml/badge.svg?branch=master%0A)](https://coveralls.io/r/onelogin/ruby-saml?branch=master%0A) [![Gem Version](https://badge.fury.io/rb/ruby-saml.svg)](http://badge.fury.io/rb/ruby-saml)
22

3+
## Updating from 1.3.x to 1.4.X
4+
5+
Version `1.4.0` is a recommended update for all Ruby SAML users as it includes security improvements.
6+
37
## Updating from 1.2.x to 1.3.X
48

59
Version `1.3.0` is a recommended update for all Ruby SAML users as it includes security fixes. It adds security improvements in order to prevent Signature wrapping attacks. [CVE-2016-5697](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-5697)

changelog.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
# RubySaml Changelog
22

3+
### 1.4.0 (October 13, 2016)
4+
* Several security improvements:
5+
* Conditions element required and unique.
6+
* AuthnStatement element required and unique.
7+
* SPNameQualifier must math the SP EntityID
8+
* Reject saml:Attribute element with same “Name” attribute
9+
* Reject empty nameID
10+
* Require Issuer element. (Must match IdP EntityID).
11+
* Destination value can't be blank (if present must match ACS URL).
12+
* Check that the EncryptedAssertion element only contains 1 Assertion element.
13+
14+
* [#335](https://github.com/onelogin/ruby-saml/pull/335) Explicitly parse as XML and fix setting of Nokogiri options.
15+
* [#345](https://github.com/onelogin/ruby-saml/pull/345)Support multiple settings.auth_context
16+
* More tests to prevent XML Signature Wrapping
17+
* [#342](https://github.com/onelogin/ruby-saml/pull/342) Correct the usage of Mutex
18+
* [352](https://github.com/onelogin/ruby-saml/pull/352) Support multiple AttributeStatement tags
19+
20+
321
### 1.3.1 (July 10, 2016)
422
* Fix response_test.rb of gem 1.3.0
523
* Add reference to Security Guidelines

lib/onelogin/ruby-saml/response.rb

Lines changed: 150 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,23 @@ def name_id_format
8888

8989
alias_method :nameid_format, :name_id_format
9090

91+
# @return [String] the NameID SPNameQualifier provided by the SAML response from the IdP.
92+
#
93+
def name_id_spnamequalifier
94+
@name_id_spnamequalifier ||=
95+
if name_id_node && name_id_node.attribute("SPNameQualifier")
96+
name_id_node.attribute("SPNameQualifier").value
97+
end
98+
end
99+
100+
# @return [String] the NameID NameQualifier provided by the SAML response from the IdP.
101+
#
102+
def name_id_namequalifier
103+
@name_id_namequalifier ||=
104+
if name_id_node && name_id_node.attribute("NameQualifier")
105+
name_id_node.attribute("NameQualifier").value
106+
end
107+
end
91108

92109
# Gets the SessionIndex from the AuthnStatement.
93110
# Could be used to be stored in the local session in order
@@ -115,7 +132,8 @@ def sessionindex
115132
# attributes['name']
116133
#
117134
# @return [Attributes] OneLogin::RubySaml::Attributes enumerable collection.
118-
#
135+
# @raise [ValidationError] if there are 2+ Attribute with the same Name
136+
#
119137
def attributes
120138
@attr_statements ||= begin
121139
attributes = Attributes.new
@@ -130,6 +148,11 @@ def attributes
130148
end
131149

132150
name = node.attributes["Name"]
151+
152+
if options[:check_duplicated_attributes] && attributes.include?(name)
153+
raise ValidationError.new("Found an Attribute element with duplicated Name")
154+
end
155+
133156
values = node.elements.collect{|e|
134157
if (e.elements.nil? || e.elements.size == 0)
135158
# SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1"
@@ -175,25 +198,31 @@ def success?
175198
#
176199
def status_code
177200
@status_code ||= begin
178-
node = REXML::XPath.first(
201+
nodes = REXML::XPath.match(
179202
document,
180203
"/p:Response/p:Status/p:StatusCode",
181204
{ "p" => PROTOCOL }
182205
)
183-
node.attributes["Value"] if node && node.attributes
206+
if nodes.size == 1
207+
node = nodes[0]
208+
node.attributes["Value"] if node && node.attributes
209+
end
184210
end
185211
end
186212

187213
# @return [String] the StatusMessage value from a SAML Response.
188214
#
189215
def status_message
190216
@status_message ||= begin
191-
node = REXML::XPath.first(
217+
nodes = REXML::XPath.match(
192218
document,
193219
"/p:Response/p:Status/p:StatusMessage",
194220
{ "p" => PROTOCOL }
195221
)
196-
node.text if node
222+
if nodes.size == 1
223+
node = nodes[0]
224+
node.text if node
225+
end
197226
end
198227
end
199228

@@ -219,26 +248,6 @@ def not_on_or_after
219248
@not_on_or_after ||= parse_time(conditions, "NotOnOrAfter")
220249
end
221250

222-
# Gets the Issuers (from Response and Assertion).
223-
# (returns the first node that matches the supplied xpath from the Response and from the Assertion)
224-
# @return [Array] Array with the Issuers (REXML::Element)
225-
#
226-
def issuers
227-
@issuers ||= begin
228-
issuers = []
229-
nodes = REXML::XPath.match(
230-
document,
231-
"/p:Response/a:Issuer",
232-
{ "p" => PROTOCOL, "a" => ASSERTION }
233-
)
234-
nodes += xpath_from_signed_assertion("/a:Issuer")
235-
nodes.each do |node|
236-
issuers << node.text if node.text
237-
end
238-
issuers.uniq
239-
end
240-
end
241-
242251
# @return [String|nil] The InResponseTo attribute from the SAML Response.
243252
#
244253
def in_response_to
@@ -303,15 +312,19 @@ def validate(collect_errors = false)
303312
:validate_id,
304313
:validate_success_status,
305314
:validate_num_assertion,
315+
:validate_no_duplicated_attributes,
306316
:validate_signed_elements,
307317
:validate_structure,
308318
:validate_in_response_to,
319+
:validate_one_conditions,
309320
:validate_conditions,
321+
:validate_one_authnstatement,
310322
:validate_audience,
311323
:validate_destination,
312324
:validate_issuer,
313325
:validate_session_expiration,
314326
:validate_subject_confirmation,
327+
:validate_name_id,
315328
:validate_signature
316329
]
317330

@@ -400,6 +413,7 @@ def validate_version
400413
# @return [Boolean] True if the SAML Response contains one unique Assertion, otherwise False
401414
#
402415
def validate_num_assertion
416+
error_msg = "SAML Response must contain 1 assertion"
403417
assertions = REXML::XPath.match(
404418
document,
405419
"//a:Assertion",
@@ -412,7 +426,35 @@ def validate_num_assertion
412426
)
413427

414428
unless assertions.size + encrypted_assertions.size == 1
415-
return append_error("SAML Response must contain 1 assertion")
429+
return append_error(error_msg)
430+
end
431+
432+
unless decrypted_document.nil?
433+
assertions = REXML::XPath.match(
434+
decrypted_document,
435+
"//a:Assertion",
436+
{ "a" => ASSERTION }
437+
)
438+
unless assertions.size == 1
439+
return append_error(error_msg)
440+
end
441+
end
442+
443+
true
444+
end
445+
446+
# Validates that there are not duplicated attributes
447+
# If fails, the error is added to the errors array
448+
# @return [Boolean] True if there are no duplicated attribute elements, otherwise False if soft=True
449+
# @raise [ValidationError] if soft == false and validation fails
450+
#
451+
def validate_no_duplicated_attributes
452+
if options[:check_duplicated_attributes]
453+
begin
454+
attributes
455+
rescue ValidationError => e
456+
return append_error(e.message)
457+
end
416458
end
417459

418460
true
@@ -516,7 +558,14 @@ def validate_audience
516558
# @return [Boolean] True if there is a Destination element that matches the Consumer Service URL, otherwise False
517559
#
518560
def validate_destination
519-
return true if destination.nil? || destination.empty? || settings.assertion_consumer_service_url.nil? || settings.assertion_consumer_service_url.empty?
561+
return true if destination.nil?
562+
563+
if destination.empty?
564+
error_msg = "The response has an empty Destination value"
565+
return append_error(error_msg)
566+
end
567+
568+
return true if settings.assertion_consumer_service_url.nil? || settings.assertion_consumer_service_url.empty?
520569

521570
unless destination == settings.assertion_consumer_service_url
522571
error_msg = "The response was received at #{destination} instead of #{settings.assertion_consumer_service_url}"
@@ -526,6 +575,34 @@ def validate_destination
526575
true
527576
end
528577

578+
# Checks that the samlp:Response/saml:Assertion/saml:Conditions element exists and is unique.
579+
# If fails, the error is added to the errors array
580+
# @return [Boolean] True if there is a conditions element and is unique
581+
#
582+
def validate_one_conditions
583+
conditions_nodes = xpath_from_signed_assertion('/a:Conditions')
584+
unless conditions_nodes.size == 1
585+
error_msg = "The Assertion must include one Conditions element"
586+
return append_error(error_msg)
587+
end
588+
589+
true
590+
end
591+
592+
# Checks that the samlp:Response/saml:Assertion/saml:AuthnStatement element exists and is unique.
593+
# If fails, the error is added to the errors array
594+
# @return [Boolean] True if there is a authnstatement element and is unique
595+
#
596+
def validate_one_authnstatement
597+
authnstatement_nodes = xpath_from_signed_assertion('/a:AuthnStatement')
598+
unless authnstatement_nodes.size == 1
599+
error_msg = "The Assertion must include one AuthnStatement element"
600+
return append_error(error_msg)
601+
end
602+
603+
true
604+
end
605+
529606
# Validates the Conditions. (If the response was initialized with the :skip_conditions option, this validation is skipped,
530607
# If the response was initialized with the :allowed_clock_drift option, the timing validations are relaxed by the allowed_clock_drift value)
531608
# @return [Boolean] True if satisfies the conditions, otherwise False if soft=True
@@ -558,6 +635,31 @@ def validate_conditions
558635
def validate_issuer
559636
return true if settings.idp_entity_id.nil?
560637

638+
issuers = []
639+
issuer_response_nodes = REXML::XPath.match(
640+
document,
641+
"/p:Response/a:Issuer",
642+
{ "p" => PROTOCOL, "a" => ASSERTION }
643+
)
644+
645+
unless issuer_response_nodes.size == 1
646+
error_msg = "Issuer of the Response not found or multiple."
647+
return append_error(error_msg)
648+
end
649+
650+
doc = decrypted_document.nil? ? document : decrypted_document
651+
issuer_assertion_nodes = xpath_from_signed_assertion("/a:Issuer")
652+
unless issuer_assertion_nodes.size == 1
653+
error_msg = "Issuer of the Assertion not found or multiple."
654+
return append_error(error_msg)
655+
end
656+
657+
nodes = issuer_response_nodes + issuer_assertion_nodes
658+
nodes.each do |node|
659+
issuers << node.text if node.text
660+
end
661+
issuers.uniq
662+
561663
issuers.each do |issuer|
562664
unless URI.parse(issuer) == URI.parse(settings.idp_entity_id)
563665
error_msg = "Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>"
@@ -631,6 +733,27 @@ def validate_subject_confirmation
631733
true
632734
end
633735

736+
# Validates the NameID element
737+
def validate_name_id
738+
if name_id_node.nil?
739+
if settings.security[:want_name_id]
740+
return append_error("No NameID element found in the assertion of the Response")
741+
end
742+
else
743+
if name_id.nil? || name_id.empty?
744+
return append_error("An empty NameID value found")
745+
end
746+
747+
unless settings.issuer.nil? || settings.issuer.empty? || name_id_spnamequalifier.nil? || name_id_spnamequalifier.empty?
748+
if name_id_spnamequalifier != settings.issuer
749+
return append_error("The SPNameQualifier value mistmatch the SP entityID value.")
750+
end
751+
end
752+
end
753+
754+
true
755+
end
756+
634757
# Validates the Signature
635758
# @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True
636759
# @raise [ValidationError] if soft == false and validation fails

lib/onelogin/ruby-saml/settings.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ def get_sp_key
155155
:logout_requests_signed => false,
156156
:logout_responses_signed => false,
157157
:want_assertions_signed => false,
158+
:want_name_id => false,
158159
:metadata_signed => false,
159160
:embed_sign => false,
160161
:digest_method => XMLSecurity::Document::SHA1,

lib/onelogin/ruby-saml/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module OneLogin
22
module RubySaml
3-
VERSION = '1.3.1'
3+
VERSION = '1.4.0'
44
end
55
end

0 commit comments

Comments
 (0)