Skip to content

Commit e76c5b3

Browse files
committed
Fix vulnerabilities: CVE-2025-25291, CVE-2025-25292: SAML authentication bypass via Signature Wrapping attack allowed due parser differential
1 parent e2da4c6 commit e76c5b3

File tree

6 files changed

+70
-22
lines changed

6 files changed

+70
-22
lines changed

lib/onelogin/ruby-saml/logoutresponse.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ def validate_success_status
150150
# @raise [ValidationError] if soft == false and validation fails
151151
#
152152
def validate_structure
153-
unless valid_saml?(document, soft)
153+
check_malformed_doc = check_malformed_doc?(settings)
154+
unless valid_saml?(document, soft, check_malformed_doc)
154155
return append_error("Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd")
155156
end
156157

lib/onelogin/ruby-saml/response.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -425,12 +425,13 @@ def validate_success_status
425425
#
426426
def validate_structure
427427
structure_error_msg = "Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd"
428-
unless valid_saml?(document, soft)
428+
check_malformed_doc = check_malformed_doc_enabled?
429+
unless valid_saml?(document, soft, check_malformed_doc)
429430
return append_error(structure_error_msg)
430431
end
431432

432433
unless decrypted_document.nil?
433-
unless valid_saml?(decrypted_document, soft)
434+
unless valid_saml?(decrypted_document, soft, check_malformed_doc)
434435
return append_error(structure_error_msg)
435436
end
436437
end
@@ -865,6 +866,8 @@ def validate_signature
865866
fingerprint = settings.get_fingerprint
866867
opts[:cert] = idp_cert
867868

869+
check_malformed_doc = check_malformed_doc_enabled?
870+
opts[:check_malformed_doc] = check_malformed_doc
868871
if fingerprint && doc.validate_document(fingerprint, @soft, opts)
869872
if settings.security[:check_idp_cert_expiration]
870873
if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert)
@@ -879,7 +882,7 @@ def validate_signature
879882
valid = false
880883
expired = false
881884
idp_certs[:signing].each do |idp_cert|
882-
valid = doc.validate_document_with_cert(idp_cert, true)
885+
valid = doc.validate_document_with_cert(idp_cert, true, check_malformed_doc)
883886
if valid
884887
if settings.security[:check_idp_cert_expiration]
885888
if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert)
@@ -1063,6 +1066,10 @@ def parse_time(node, attribute)
10631066
Time.parse(node.attributes[attribute])
10641067
end
10651068
end
1069+
1070+
def check_malformed_doc_enabled?
1071+
check_malformed_doc?(settings)
1072+
end
10661073
end
10671074
end
10681075
end

lib/onelogin/ruby-saml/saml_message.rb

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,13 @@ def id(document)
6363
# Validates the SAML Message against the specified schema.
6464
# @param document [REXML::Document] The message that will be validated
6565
# @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the message is invalid or not)
66+
# @param check_malformed_doc [Boolean] check_malformed_doc Enable or Disable the check for malformed XML
6667
# @return [Boolean] True if the XML is valid, otherwise False, if soft=True
6768
# @raise [ValidationError] if soft == false and validation fails
6869
#
69-
def valid_saml?(document, soft = true)
70+
def valid_saml?(document, soft = true, check_malformed_doc = true)
7071
begin
71-
xml = Nokogiri::XML(document.to_s) do |config|
72-
config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
73-
end
72+
xml = XMLSecurity::BaseDocument.safe_load_xml(document, check_malformed_doc)
7473
rescue Exception => error
7574
return false if soft
7675
raise ValidationError.new("XML load failed: #{error.message}")
@@ -163,6 +162,12 @@ def inflate(deflated)
163162
def deflate(inflated)
164163
Zlib::Deflate.deflate(inflated, 9)[2..-5]
165164
end
165+
166+
def check_malformed_doc?(settings)
167+
default_value = OneLogin::RubySaml::Settings::DEFAULTS[:check_malformed_doc]
168+
169+
settings.nil? ? default_value : settings.check_malformed_doc
170+
end
166171
end
167172
end
168173
end

lib/onelogin/ruby-saml/settings.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def initialize(overrides = {}, keep_security_attributes = false)
5353
attr_accessor :compress_request
5454
attr_accessor :compress_response
5555
attr_accessor :double_quote_xml_attribute_values
56+
attr_accessor :check_malformed_doc
5657
attr_accessor :passive
5758
attr_accessor :protocol_binding
5859
attr_accessor :attributes_index
@@ -259,6 +260,7 @@ def get_sp_key
259260
:compress_request => true,
260261
:compress_response => true,
261262
:soft => true,
263+
:check_malformed_doc => true,
262264
:double_quote_xml_attribute_values => false,
263265
:security => {
264266
:authn_requests_signed => false,

lib/onelogin/ruby-saml/slo_logoutrequest.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,8 @@ def validate_not_on_or_after
199199
# @raise [ValidationError] if soft == false and validation fails
200200
#
201201
def validate_structure
202-
unless valid_saml?(document, soft)
202+
check_malformed_doc = check_malformed_doc?(settings)
203+
unless valid_saml?(document, soft, check_malformed_doc)
203204
return append_error("Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd")
204205
end
205206

lib/xml_security.rb

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,36 @@ class BaseDocument < REXML::Document
4242
NOKOGIRI_OPTIONS = Nokogiri::XML::ParseOptions::STRICT |
4343
Nokogiri::XML::ParseOptions::NONET
4444

45+
# Safety load the SAML Message XML
46+
# @param document [REXML::Document] The message to be loaded
47+
# @param check_malformed_doc [Boolean] check_malformed_doc Enable or Disable the check for malformed XML
48+
# @return [Nokogiri::XML] The nokogiri document
49+
# @raise [ValidationError] If there was a problem loading the SAML Message XML
50+
def self.safe_load_xml(document, check_malformed_doc = true)
51+
doc_str = document.to_s
52+
if doc_str.include?("<!DOCTYPE")
53+
raise StandardError.new("Dangerous XML detected. No Doctype nodes allowed")
54+
end
55+
56+
begin
57+
xml = Nokogiri::XML(doc_str) do |config|
58+
config.options = self::NOKOGIRI_OPTIONS
59+
end
60+
rescue StandardError => error
61+
raise StandardError.new(error.message)
62+
end
63+
64+
if xml.internal_subset
65+
raise StandardError.new("Dangerous XML detected. No Doctype nodes allowed")
66+
end
67+
68+
unless xml.errors.empty?
69+
raise StandardError.new("There were XML errors when parsing: #{xml.errors}") if check_malformed_doc
70+
end
71+
72+
xml
73+
end
74+
4575
def canon_algorithm(element)
4676
algorithm = element
4777
if algorithm.is_a?(REXML::Element)
@@ -114,10 +144,8 @@ def uuid
114144
#<KeyInfo />
115145
#<Object />
116146
#</Signature>
117-
def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_method = SHA1)
118-
noko = Nokogiri::XML(self.to_s) do |config|
119-
config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
120-
end
147+
def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_method = SHA1, check_malformed_doc = true)
148+
noko = XMLSecurity::BaseDocument.safe_load_xml(self.to_s, check_malformed_doc)
121149

122150
signature_element = REXML::Element.new("ds:Signature").add_namespace('ds', DSIG)
123151
signed_info_element = signature_element.add_element("ds:SignedInfo")
@@ -139,9 +167,7 @@ def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_
139167
reference_element.add_element("ds:DigestValue").text = compute_digest(canon_doc, algorithm(digest_method_element))
140168

141169
# add SignatureValue
142-
noko_sig_element = Nokogiri::XML(signature_element.to_s) do |config|
143-
config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
144-
end
170+
noko_sig_element = XMLSecurity::BaseDocument.safe_load_xml(signature_element.to_s, check_malformed_doc)
145171

146172
noko_signed_info_element = noko_sig_element.at_xpath('//ds:Signature/ds:SignedInfo', 'ds' => DSIG)
147173
canon_string = noko_signed_info_element.canonicalize(canon_algorithm(C14N))
@@ -237,10 +263,12 @@ def validate_document(idp_cert_fingerprint, soft = true, options = {})
237263
end
238264
end
239265
end
240-
validate_signature(base64_cert, soft)
266+
check_malformed_doc = true
267+
check_malformed_doc = options[:check_malformed_doc] if options.key?(:check_malformed_doc)
268+
validate_signature(base64_cert, soft, check_malformed_doc)
241269
end
242270

243-
def validate_document_with_cert(idp_cert, soft = true)
271+
def validate_document_with_cert(idp_cert, soft = true, check_malformed_doc = true)
244272
# get cert from response
245273
cert_element = REXML::XPath.first(
246274
self,
@@ -264,13 +292,17 @@ def validate_document_with_cert(idp_cert, soft = true)
264292
else
265293
base64_cert = Base64.encode64(idp_cert.to_pem)
266294
end
267-
validate_signature(base64_cert, true)
295+
validate_signature(base64_cert, true, check_malformed_doc)
268296
end
269297

270-
def validate_signature(base64_cert, soft = true)
298+
def validate_signature(base64_cert, soft = true, check_malformed_doc = true)
271299

272-
document = Nokogiri::XML(self.to_s) do |config|
273-
config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
300+
begin
301+
document = XMLSecurity::BaseDocument.safe_load_xml(self, check_malformed_doc)
302+
rescue StandardError => error
303+
@errors << error.message
304+
return false if soft
305+
raise ValidationError.new("XML load failed: #{error.message}")
274306
end
275307

276308
# create a rexml document

0 commit comments

Comments
 (0)