• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SAML-Toolkits / ruby-saml / 13818775034

12 Mar 2025 06:24PM UTC coverage: 97.472% (-0.4%) from 97.833%
13818775034

push

github

web-flow
Merge pull request #750 from SAML-Toolkits/security_fix_1.18.0

Security fixes: CVE-2025-25291, CVE-2025-25292 and CVE-2025-25293

117 of 125 new or added lines in 6 files covered. (93.6%)

1 existing line in 1 file now uncovered.

2005 of 2057 relevant lines covered (97.47%)

2137.27 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.07
/lib/xml_security.rb
1
# The contents of this file are subject to the terms
2
# of the Common Development and Distribution License
3
# (the License). You may not use this file except in
4
# compliance with the License.
5
#
6
# You can obtain a copy of the License at
7
# https://opensso.dev.java.net/public/CDDLv1.0.html or
8
# opensso/legal/CDDLv1.0.txt
9
# See the License for the specific language governing
10
# permission and limitations under the License.
11
#
12
# When distributing Covered Code, include this CDDL
13
# Header Notice in each file and include the License file
14
# at opensso/legal/CDDLv1.0.txt.
15
# If applicable, add the following below the CDDL Header,
16
# with the fields enclosed by brackets [] replaced by
17
# your own identifying information:
18
# "Portions Copyrighted [year] [name of copyright owner]"
19
#
20
# $Id: xml_sec.rb,v 1.6 2007/10/24 00:28:41 todddd Exp $
21
#
22
# Copyright 2007 Sun Microsystems Inc. All Rights Reserved
23
# Portions Copyrighted 2007 Todd W Saxton.
24

25
require 'rubygems'
33✔
26
require "rexml/document"
33✔
27
require "rexml/xpath"
33✔
28
require "openssl"
33✔
29
require 'nokogiri'
33✔
30
require "digest/sha1"
33✔
31
require "digest/sha2"
33✔
32
require "onelogin/ruby-saml/utils"
33✔
33
require "onelogin/ruby-saml/error_handling"
33✔
34

35
module XMLSecurity
33✔
36

37
  class BaseDocument < REXML::Document
33✔
38
    REXML::Document::entity_expansion_limit = 0
33✔
39

40
    C14N            = "http://www.w3.org/2001/10/xml-exc-c14n#"
33✔
41
    DSIG            = "http://www.w3.org/2000/09/xmldsig#"
33✔
42
    NOKOGIRI_OPTIONS = Nokogiri::XML::ParseOptions::STRICT |
33✔
43
                       Nokogiri::XML::ParseOptions::NONET
44

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)
33✔
51
      doc_str = document.to_s
9,405✔
52
      if doc_str.include?("<!DOCTYPE")
9,405✔
53
       raise StandardError.new("Dangerous XML detected. No Doctype nodes allowed")
66✔
54
      end
55

56
      begin
3,677✔
57
        xml = Nokogiri::XML(doc_str) do |config|
9,339✔
58
          config.options = self::NOKOGIRI_OPTIONS
9,339✔
59
        end
60
      rescue StandardError => error
NEW
61
        raise StandardError.new(error.message)
×
62
      end
63

64
      if xml.internal_subset
9,339✔
NEW
65
        raise StandardError.new("Dangerous XML detected. No Doctype nodes allowed")
×
66
      end
67

68
      unless xml.errors.empty?
9,339✔
NEW
69
        raise StandardError.new("There were XML errors when parsing: #{xml.errors}") if check_malformed_doc
×
70
      end
71

72
      xml
9,339✔
73
    end
74

75
    def canon_algorithm(element)
33✔
76
      algorithm = element
13,101✔
77
      if algorithm.is_a?(REXML::Element)
13,101✔
78
        algorithm = element.attribute('Algorithm').value
10,692✔
79
      end
80

81
      case algorithm
12,307✔
82
        when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
83
             "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
84
          Nokogiri::XML::XML_C14N_1_0
311✔
85
        when "http://www.w3.org/2006/12/xml-c14n11",
86
             "http://www.w3.org/2006/12/xml-c14n11#WithComments"
87
          Nokogiri::XML::XML_C14N_1_1
63✔
88
        else
89
          Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
12,705✔
90
      end
91
    end
92

93
    def algorithm(element)
33✔
94
      algorithm = element
15,576✔
95
      if algorithm.is_a?(REXML::Element)
15,576✔
96
        algorithm = element.attribute("Algorithm").value
8,448✔
97
      end
98

99
      algorithm = algorithm && algorithm =~ /(rsa-)?sha(.*?)$/i && $2.to_i
15,576✔
100

101
      case algorithm
14,632✔
102
      when 256 then OpenSSL::Digest::SHA256
957✔
103
      when 384 then OpenSSL::Digest::SHA384
363✔
104
      when 512 then OpenSSL::Digest::SHA512
660✔
105
      else
106
        OpenSSL::Digest::SHA1
13,596✔
107
      end
108
    end
109

110
  end
111

112
  class Document < BaseDocument
33✔
113
    RSA_SHA1        = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
33✔
114
    RSA_SHA256      = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
33✔
115
    RSA_SHA384      = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"
33✔
116
    RSA_SHA512      = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"
33✔
117
    SHA1            = "http://www.w3.org/2000/09/xmldsig#sha1"
33✔
118
    SHA256          = 'http://www.w3.org/2001/04/xmlenc#sha256'
33✔
119
    SHA384          = "http://www.w3.org/2001/04/xmldsig-more#sha384"
33✔
120
    SHA512          = 'http://www.w3.org/2001/04/xmlenc#sha512'
33✔
121
    ENVELOPED_SIG   = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
33✔
122
    INC_PREFIX_LIST = "#default samlp saml ds xs xsi md"
33✔
123

124
    attr_writer :uuid
33✔
125

126
    def uuid
33✔
127
      @uuid ||= begin
858✔
128
        document.root.nil? ? nil : document.root.attributes['ID']
132✔
129
      end
231✔
130
    end
131

132
    #<Signature>
133
      #<SignedInfo>
134
        #<CanonicalizationMethod />
135
        #<SignatureMethod />
136
        #<Reference>
137
           #<Transforms>
138
           #<DigestMethod>
139
           #<DigestValue>
140
        #</Reference>
141
        #<Reference /> etc.
142
      #</SignedInfo>
143
      #<SignatureValue />
144
      #<KeyInfo />
145
      #<Object />
146
    #</Signature>
147
    def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_method = SHA1, check_malformed_doc = true)
33✔
148
      noko = XMLSecurity::BaseDocument.safe_load_xml(self.to_s, check_malformed_doc)
1,089✔
149

150
      signature_element = REXML::Element.new("ds:Signature").add_namespace('ds', DSIG)
1,089✔
151
      signed_info_element = signature_element.add_element("ds:SignedInfo")
1,089✔
152
      signed_info_element.add_element("ds:CanonicalizationMethod", {"Algorithm" => C14N})
1,089✔
153
      signed_info_element.add_element("ds:SignatureMethod", {"Algorithm"=>signature_method})
1,089✔
154

155
      # Add Reference
156
      reference_element = signed_info_element.add_element("ds:Reference", {"URI" => "##{uuid}"})
1,089✔
157

158
      # Add Transforms
159
      transforms_element = reference_element.add_element("ds:Transforms")
1,089✔
160
      transforms_element.add_element("ds:Transform", {"Algorithm" => ENVELOPED_SIG})
1,089✔
161
      c14element = transforms_element.add_element("ds:Transform", {"Algorithm" => C14N})
1,089✔
162
      c14element.add_element("ec:InclusiveNamespaces", {"xmlns:ec" => C14N, "PrefixList" => INC_PREFIX_LIST})
1,089✔
163

164
      digest_method_element = reference_element.add_element("ds:DigestMethod", {"Algorithm" => digest_method})
1,089✔
165
      inclusive_namespaces = INC_PREFIX_LIST.split(" ")
1,089✔
166
      canon_doc = noko.canonicalize(canon_algorithm(C14N), inclusive_namespaces)
1,089✔
167
      reference_element.add_element("ds:DigestValue").text = compute_digest(canon_doc, algorithm(digest_method_element))
1,089✔
168

169
      # add SignatureValue
170
      noko_sig_element = XMLSecurity::BaseDocument.safe_load_xml(signature_element.to_s, check_malformed_doc)
1,089✔
171

172
      noko_signed_info_element = noko_sig_element.at_xpath('//ds:Signature/ds:SignedInfo', 'ds' => DSIG)
1,089✔
173
      canon_string = noko_signed_info_element.canonicalize(canon_algorithm(C14N))
1,089✔
174

175
      signature = compute_signature(private_key, algorithm(signature_method).new, canon_string)
1,089✔
176
      signature_element.add_element("ds:SignatureValue").text = signature
1,089✔
177

178
      # add KeyInfo
179
      key_info_element       = signature_element.add_element("ds:KeyInfo")
1,089✔
180
      x509_element           = key_info_element.add_element("ds:X509Data")
1,089✔
181
      x509_cert_element      = x509_element.add_element("ds:X509Certificate")
1,089✔
182
      if certificate.is_a?(String)
1,089✔
183
        certificate = OpenSSL::X509::Certificate.new(certificate)
132✔
184
      end
185
      x509_cert_element.text = Base64.encode64(certificate.to_der).gsub(/\n/, "")
1,089✔
186

187
      # add the signature
188
      issuer_element = elements["//saml:Issuer"]
1,089✔
189
      if issuer_element
1,089✔
190
        root.insert_after(issuer_element, signature_element)
264✔
191
      elsif first_child = root.children[0]
821✔
192
        root.insert_before(first_child, signature_element)
660✔
193
      else
194
        root.add_element(signature_element)
165✔
195
      end
196
    end
197

198
    protected
33✔
199

200
    def compute_signature(private_key, signature_algorithm, document)
33✔
201
      Base64.encode64(private_key.sign(signature_algorithm, document)).gsub(/\n/, "")
1,089✔
202
    end
203

204
    def compute_digest(document, digest_algorithm)
33✔
205
      digest = digest_algorithm.digest(document)
1,089✔
206
      Base64.encode64(digest).strip
1,089✔
207
    end
208

209
  end
210

211
  class SignedDocument < BaseDocument
33✔
212
    include OneLogin::RubySaml::ErrorHandling
33✔
213

214
    attr_writer :signed_element_id
33✔
215

216
    def initialize(response, errors = [])
33✔
217
      super(response)
12,837✔
218
      @errors = errors
12,837✔
219
      reset_elements
12,837✔
220
    end
221

222
    def reset_elements
33✔
223
      @referenced_xml = nil
18,315✔
224
      @cached_signed_info = nil
18,315✔
225
      @signature = nil
18,315✔
226
      @signature_algorithm = nil
18,315✔
227
      @ref = nil
18,315✔
228
      @processed = false
18,315✔
229
    end
230

231
    def processed
33✔
232
      @processed
8,811✔
233
    end
234

235
    def referenced_xml
33✔
236
      @referenced_xml
3,993✔
237
    end
238

239
    def signed_element_id
33✔
240
      @signed_element_id ||= extract_signed_element_id
13,893✔
241
    end
242

243
    # Validates the referenced_xml, which is the signed part of the document
244
    def validate_document(idp_cert_fingerprint, soft = true, options = {})
33✔
245
      # get cert from response
246
      cert_element = REXML::XPath.first(
1,947✔
247
        self,
248
        "//ds:X509Certificate",
249
        { "ds"=>DSIG }
250
      )
251

252
      if cert_element
1,947✔
253
        base64_cert = OneLogin::RubySaml::Utils.element_text(cert_element)
1,716✔
254
        cert_text = Base64.decode64(base64_cert)
1,716✔
255
        begin
674✔
256
          cert = OpenSSL::X509::Certificate.new(cert_text)
1,716✔
257
        rescue OpenSSL::X509::CertificateError => _e
258
          return append_error("Document Certificate Error", soft)
33✔
259
        end
260

261
        if options[:fingerprint_alg]
1,683✔
262
          fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(options[:fingerprint_alg]).new
792✔
263
        else
264
          fingerprint_alg = OpenSSL::Digest.new('SHA1')
891✔
265
        end
266
        fingerprint = fingerprint_alg.hexdigest(cert.to_der)
1,683✔
267

268
        # check cert matches registered idp cert
269
        if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
1,683✔
270
          return append_error("Fingerprint mismatch", soft)
198✔
271
        end
272
        base64_cert = Base64.encode64(cert.to_der)
1,485✔
273
      else
274
        if options[:cert]
231✔
275
          base64_cert = Base64.encode64(options[:cert].to_pem)
132✔
276
        else
277
          if soft
99✔
278
            return false
66✔
279
          else
280
            return append_error("Certificate element missing in response (ds:X509Certificate) and not cert provided at settings", soft)
33✔
281
          end
282
        end
283
      end
284
      validate_signature(base64_cert, soft)
1,617✔
285
    end
286

287
    def validate_document_with_cert(idp_cert, soft = true)
33✔
288
      # get cert from response
289
      cert_element = REXML::XPath.first(
429✔
290
        self,
291
        "//ds:X509Certificate",
292
        { "ds"=>DSIG }
293
      )
294

295
      if cert_element
429✔
296
        base64_cert = OneLogin::RubySaml::Utils.element_text(cert_element)
363✔
297
        cert_text = Base64.decode64(base64_cert)
363✔
298
        begin
141✔
299
          cert = OpenSSL::X509::Certificate.new(cert_text)
363✔
300
        rescue OpenSSL::X509::CertificateError => _e
301
          return append_error("Document Certificate Error", soft)
66✔
302
        end
303

304
        # check saml response cert matches provided idp cert
305
        if idp_cert.to_pem != cert.to_pem
297✔
306
          return append_error("Certificate of the Signature element does not match provided certificate", soft)
198✔
307
        end
308
      end
309

310
      encoded_idp_cert = Base64.encode64(idp_cert.to_pem)
165✔
311
      validate_signature(encoded_idp_cert, true)
165✔
312
    end
313

314
    def cache_referenced_xml(soft, check_malformed_doc = true)
33✔
315
      reset_elements
5,478✔
316
      @processed = true
5,478✔
317

318
      begin
2,156✔
319
        nokogiri_document = XMLSecurity::BaseDocument.safe_load_xml(self, check_malformed_doc)
5,478✔
320
      rescue StandardError => error
NEW
321
        @errors << error.message
×
NEW
322
        return false if soft
×
NEW
323
        raise ValidationError.new("XML load failed: #{error.message}")
×
324
      end
325

326
      # create a rexml document
327
      @working_copy ||= REXML::Document.new(self.to_s).root
5,478✔
328

329
      # get signature node
330
      sig_element = REXML::XPath.first(
5,478✔
331
          @working_copy,
332
          "//ds:Signature",
333
          {"ds"=>DSIG}
334
      )
335

336
      return if sig_element.nil?
5,478✔
337

338
      # signature method
339
      sig_alg_value = REXML::XPath.first(
5,379✔
340
        sig_element,
341
        "./ds:SignedInfo/ds:SignatureMethod",
342
        {"ds"=>DSIG}
343
      )
344
      @signature_algorithm = algorithm(sig_alg_value)
5,379✔
345

346
      # get signature
347
      base64_signature = REXML::XPath.first(
5,379✔
348
        sig_element,
349
        "./ds:SignatureValue",
350
        {"ds" => DSIG}
351
      )
352

353
      return if base64_signature.nil?
5,379✔
354

355
      base64_signature_text = OneLogin::RubySaml::Utils.element_text(base64_signature)
5,346✔
356
      @signature = base64_signature_text.nil? ? nil : Base64.decode64(base64_signature_text)
5,346✔
357

358
      # canonicalization method
359
      canon_algorithm = canon_algorithm REXML::XPath.first(
5,346✔
360
        sig_element,
361
        './ds:SignedInfo/ds:CanonicalizationMethod',
362
        'ds' => DSIG
363
      )
364

365
      noko_sig_element = nokogiri_document.at_xpath('//ds:Signature', 'ds' => DSIG)
5,346✔
366
      noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => DSIG)
5,346✔
367

368
      @cached_signed_info = noko_signed_info_element.canonicalize(canon_algorithm)
5,346✔
369

370
      ### Now get the @referenced_xml to use?
371
      rexml_signed_info = REXML::Document.new(@cached_signed_info.to_s).root
5,346✔
372

373
      noko_sig_element.remove
5,346✔
374

375
      # get inclusive namespaces
376
      inclusive_namespaces = extract_inclusive_namespaces
5,346✔
377

378
      # check digests
379
      @ref = REXML::XPath.first(rexml_signed_info, "./ds:Reference", {"ds"=>DSIG})
5,346✔
380
      return if @ref.nil?
5,346✔
381

382
      reference_nodes = nokogiri_document.xpath("//*[@ID=$id]", nil, { 'id' => extract_signed_element_id })
5,346✔
383

384
      hashed_element = reference_nodes[0]
5,346✔
385
      return if hashed_element.nil?
5,346✔
386

387
      canon_algorithm = canon_algorithm REXML::XPath.first(
5,346✔
388
        rexml_signed_info,
389
        './ds:CanonicalizationMethod',
390
        { "ds" => DSIG }
391
      )
392

393
      canon_algorithm = process_transforms(@ref, canon_algorithm)
5,346✔
394

395
      @referenced_xml = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
5,346✔
396
    end
397

398
    def validate_signature(base64_cert, soft = true)
33✔
399
      if !@processed
2,013✔
400
        cache_referenced_xml(soft)
1,089✔
401
      end
402

403
      return append_error("No Signature Algorithm Method found", soft) if @signature_algorithm.nil?  
2,013✔
404
      return append_error("No Signature node found", soft) if @signature.nil?  
2,013✔
405
      return append_error("No canonized SignedInfo ", soft) if @cached_signed_info.nil?
2,013✔
406
      return append_error("No Reference node found", soft) if @ref.nil?
2,013✔
407
      return append_error("No referenced XML", soft) if @referenced_xml.nil?
2,013✔
408

409
      # get certificate object
410
      cert_text = Base64.decode64(base64_cert)
2,013✔
411
      cert = OpenSSL::X509::Certificate.new(cert_text)
2,013✔
412

413
      digest_algorithm = algorithm(REXML::XPath.first(
2,013✔
414
        @ref,
415
        "./ds:DigestMethod",
416
        { "ds" => DSIG }
417
      ))
418
      hash = digest_algorithm.digest(@referenced_xml)
2,013✔
419
      encoded_digest_value = REXML::XPath.first(
2,013✔
420
        @ref,
421
        "./ds:DigestValue",
422
        { "ds" => DSIG }
423
      )
424
      encoded_digest_value_text = OneLogin::RubySaml::Utils.element_text(encoded_digest_value)
2,013✔
425
      digest_value = encoded_digest_value_text.nil? ? nil : Base64.decode64(encoded_digest_value_text)
2,013✔
426

427
      # Compare the computed "hash" with the "signed" hash
428
      unless hash && hash == digest_value
2,013✔
429
        return append_error("Digest mismatch", soft)
248✔
430
      end
431

432
      # verify signature
433
      unless cert.public_key.verify(@signature_algorithm.new, @signature, @cached_signed_info)
1,749✔
434
        return append_error("Key validation error", soft)
62✔
435
      end
436

437
      return true
1,581✔
438
    end
439

440
    private
33✔
441

442
    def process_transforms(ref, canon_algorithm)
33✔
443
      transforms = REXML::XPath.match(
5,346✔
444
        ref,
445
        "./ds:Transforms/ds:Transform",
446
        { "ds" => DSIG }
447
      )
448

449
      transforms.each do |transform_element|
5,346✔
450
        if transform_element.attributes && transform_element.attributes["Algorithm"]
10,560✔
451
          algorithm = transform_element.attributes["Algorithm"]
10,560✔
452
          case algorithm
9,920✔
453
            when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
454
                 "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
455
              canon_algorithm = Nokogiri::XML::XML_C14N_1_0
×
456
            when "http://www.w3.org/2006/12/xml-c14n11",
457
                 "http://www.w3.org/2006/12/xml-c14n11#WithComments"
458
              canon_algorithm = Nokogiri::XML::XML_C14N_1_1
×
459
            when "http://www.w3.org/2001/10/xml-exc-c14n#",
460
                 "http://www.w3.org/2001/10/xml-exc-c14n#WithComments"
461
              canon_algorithm = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
4,898✔
462
          end
463
        end
464
      end
465

466
      canon_algorithm
5,346✔
467
    end
468

469
    def digests_match?(hash, digest_value)
33✔
UNCOV
470
      hash == digest_value
×
471
    end
472

473
    def extract_signed_element_id
33✔
474
      reference_element = REXML::XPath.first(
10,197✔
475
        self,
476
        "//ds:Signature/ds:SignedInfo/ds:Reference",
477
        {"ds"=>DSIG}
478
      )
479

480
      return nil if reference_element.nil?
10,197✔
481

482
      sei = reference_element.attribute("URI").value[1..-1]
9,702✔
483
      sei.nil? ? reference_element.parent.parent.parent.attribute("ID").value : sei
9,702✔
484
    end
485

486
    def extract_inclusive_namespaces
33✔
487
      element = REXML::XPath.first(
5,445✔
488
        self,
489
        "//ec:InclusiveNamespaces",
490
        { "ec" => C14N }
491
      )
492
      if element
5,445✔
493
        prefix_list = element.attributes.get_attribute("PrefixList").value
858✔
494
        prefix_list.split(" ")
858✔
495
      else
496
        nil
1,251✔
497
      end
498
    end
499

500
  end
501
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc