Skip to content

Commit af330c2

Browse files
committed
Merge pull request #247 from onelogin/prevent_entity_expansion
Security improvement: Avoid entity expansion (XEE attacks)
2 parents 0aed0dc + a2e5318 commit af330c2

4 files changed

Lines changed: 59 additions & 6 deletions

File tree

lib/onelogin/ruby-saml/saml_message.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,17 @@ def id(document)
6363
# @raise [ValidationError] if soft == false and validation fails
6464
#
6565
def valid_saml?(document, soft = true)
66-
xml = Nokogiri::XML(document.to_s)
66+
begin
67+
xml = Nokogiri::XML(document.to_s) do |config|
68+
config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
69+
end
70+
rescue Exception => error
71+
return false if soft
72+
validation_error("XML load failed: #{error.message}")
73+
end
6774

6875
SamlMessage.schema.validate(xml).map do |error|
69-
break false if soft
76+
return false if soft
7077
validation_error("#{error.message}\n\n#{xml.to_s}")
7178
end
7279
end

lib/xml_security.rb

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,12 @@
3434
module XMLSecurity
3535

3636
class BaseDocument < REXML::Document
37+
REXML::Document::entity_expansion_limit = 0
3738

3839
C14N = "http://www.w3.org/2001/10/xml-exc-c14n#"
3940
DSIG = "http://www.w3.org/2000/09/xmldsig#"
41+
NOKOGIRI_OPTIONS = Nokogiri::XML::ParseOptions::STRICT |
42+
Nokogiri::XML::ParseOptions::NONET
4043

4144
def canon_algorithm(element)
4245
algorithm = element
@@ -106,7 +109,9 @@ def uuid
106109
#<Object />
107110
#</Signature>
108111
def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_method = SHA1)
109-
noko = Nokogiri.parse(self.to_s)
112+
noko = Nokogiri.parse(self.to_s) do |options|
113+
options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
114+
end
110115

111116
signature_element = REXML::Element.new("ds:Signature").add_namespace('ds', DSIG)
112117
signed_info_element = signature_element.add_element("ds:SignedInfo")
@@ -128,7 +133,10 @@ def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_
128133
reference_element.add_element("ds:DigestValue").text = compute_digest(canon_doc, algorithm(digest_method_element))
129134

130135
# add SignatureValue
131-
noko_sig_element = Nokogiri.parse(signature_element.to_s)
136+
noko_sig_element = Nokogiri.parse(signature_element.to_s) do |options|
137+
options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
138+
end
139+
132140
noko_signed_info_element = noko_sig_element.at_xpath('//ds:Signature/ds:SignedInfo', 'ds' => DSIG)
133141
canon_string = noko_signed_info_element.canonicalize(canon_algorithm(C14N))
134142

@@ -178,7 +186,10 @@ class SignedDocument < BaseDocument
178186
def initialize(response, errors = [])
179187
super(response)
180188
@errors = errors
181-
extract_signed_element_id
189+
end
190+
191+
def signed_element_id
192+
@signed_element_id ||= extract_signed_element_id
182193
end
183194

184195
def validate_document(idp_cert_fingerprint, soft = true, options = {})
@@ -221,7 +232,9 @@ def validate_signature(base64_cert, soft = true)
221232
# check for inclusive namespaces
222233
inclusive_namespaces = extract_inclusive_namespaces
223234

224-
document = Nokogiri.parse(self.to_s)
235+
document = Nokogiri.parse(self.to_s) do |options|
236+
options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
237+
end
225238

226239
# create a working copy so we don't modify the original
227240
@working_copy ||= REXML::Document.new(self.to_s).root

test/response_test.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,26 @@ class RubySamlTest < Minitest::Test
5353
assert_includes ampersands_response.errors, "SAML Response must contain 1 assertion"
5454
end
5555

56+
describe "Prevent XEE attack" do
57+
before do
58+
@response = OneLogin::RubySaml::Response.new(fixture(:attackxee))
59+
end
60+
61+
it "false when evil attack vector is present, soft = true" do
62+
@response.soft = true
63+
assert !@response.send(:validate_structure)
64+
assert_includes @response.errors, "Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd"
65+
end
66+
67+
it "raise when evil attack vector is present, soft = false " do
68+
@response.soft = false
69+
70+
assert_raises(OneLogin::RubySaml::ValidationError) do
71+
@response.send(:validate_structure)
72+
end
73+
end
74+
end
75+
5676
it "adapt namespace" do
5777
refute_nil response.nameid
5878
refute_nil response_without_attributes.nameid

test/responses/attackxee.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0"?>
2+
<!DOCTYPE lolz [
3+
<!ENTITY lol "lol">
4+
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
5+
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
6+
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
7+
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
8+
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
9+
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
10+
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
11+
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
12+
]>
13+
<lolz>&lol9;</lolz>

0 commit comments

Comments
 (0)