From b6fa04428c608ac46ea9337a5bce8b068f1a2287 Mon Sep 17 00:00:00 2001 From: Dan Jensen Date: Tue, 24 Aug 2021 16:47:16 -0500 Subject: [PATCH] Replace MAX_BYTE_SIZE constant with setting The MAX_BYTE_SIZE constant did not allow for customization, which is necessary for cases where legitimate SAML responses are larger than 250,000 bytes. This replaces the constant with a setting, which has a default value of 250,000 bytes, but can be customized like any other setting. --- README.md | 21 ++++++++++++++ lib/onelogin/ruby-saml/logoutresponse.rb | 2 +- lib/onelogin/ruby-saml/response.rb | 2 +- lib/onelogin/ruby-saml/saml_message.rb | 9 +++--- lib/onelogin/ruby-saml/settings.rb | 2 ++ lib/onelogin/ruby-saml/slo_logoutrequest.rb | 2 +- test/saml_message_test.rb | 32 ++++++++++++++------- test/settings_test.rb | 3 +- 8 files changed, 54 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 2cfb609bb..400fda406 100644 --- a/README.md +++ b/README.md @@ -816,6 +816,27 @@ response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], :allowed_cloc Make sure to keep the value as comfortably small as possible to keep security risks to a minimum. +## Deflation Limit + +To protect against decompression bombs (a form of DoS attack), SAML messages are limited to 250,000 bytes by default. +Sometimes legitimate SAML messages will exceed this limit, +for example due to custom claims like including groups a user is a member of. +If you want to customize this limit, you need to provide a different setting when initializing the response object. +Example: + +```ruby +def consume + response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], { settings: saml_settings }) + ... +end + +private + +def saml_settings + OneLogin::RubySaml::Settings.new(message_max_bytesize: 500_000) +end +``` + ## Attribute Service To request attributes from the IdP the SP needs to provide an attribute service within it's metadata and reference the index in the assertion. diff --git a/lib/onelogin/ruby-saml/logoutresponse.rb b/lib/onelogin/ruby-saml/logoutresponse.rb index 47b496e86..06c7ebe4a 100644 --- a/lib/onelogin/ruby-saml/logoutresponse.rb +++ b/lib/onelogin/ruby-saml/logoutresponse.rb @@ -43,7 +43,7 @@ def initialize(response, settings = nil, options = {}) end @options = options - @response = decode_raw_saml(response) + @response = decode_raw_saml(response, settings) @document = XMLSecurity::SignedDocument.new(@response) end diff --git a/lib/onelogin/ruby-saml/response.rb b/lib/onelogin/ruby-saml/response.rb index 505e93651..fceb0026e 100644 --- a/lib/onelogin/ruby-saml/response.rb +++ b/lib/onelogin/ruby-saml/response.rb @@ -63,7 +63,7 @@ def initialize(response, options = {}) end end - @response = decode_raw_saml(response) + @response = decode_raw_saml(response, settings) @document = XMLSecurity::SignedDocument.new(@response, @errors) if assertion_encrypted? diff --git a/lib/onelogin/ruby-saml/saml_message.rb b/lib/onelogin/ruby-saml/saml_message.rb index e680d5c1a..d09f55e2d 100644 --- a/lib/onelogin/ruby-saml/saml_message.rb +++ b/lib/onelogin/ruby-saml/saml_message.rb @@ -22,8 +22,6 @@ class SamlMessage BASE64_FORMAT = %r(\A([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?\Z) @@mutex = Mutex.new - MAX_BYTE_SIZE = 250000 - # @return [Nokogiri::XML::Schema] Gets the schema object of the SAML 2.0 Protocol schema # def self.schema @@ -88,11 +86,12 @@ def valid_saml?(document, soft = true) # @param saml [String] The deflated and encoded SAML Message # @return [String] The plain SAML Message # - def decode_raw_saml(saml) + def decode_raw_saml(saml, settings = nil) return saml unless base64_encoded?(saml) - if saml.bytesize > MAX_BYTE_SIZE - raise ValidationError.new("Encoded SAML Message exceeds " + MAX_BYTE_SIZE.to_s + " bytes, so was rejected") + settings = OneLogin::RubySaml::Settings.new if settings.nil? + if saml.bytesize > settings.message_max_bytesize + raise ValidationError.new("Encoded SAML Message exceeds " + settings.message_max_bytesize.to_s + " bytes, so was rejected") end decoded = decode(saml) diff --git a/lib/onelogin/ruby-saml/settings.rb b/lib/onelogin/ruby-saml/settings.rb index 1a2f78e2d..58e1afcea 100644 --- a/lib/onelogin/ruby-saml/settings.rb +++ b/lib/onelogin/ruby-saml/settings.rb @@ -54,6 +54,7 @@ def initialize(overrides = {}, keep_security_attributes = false) attr_accessor :compress_request attr_accessor :compress_response attr_accessor :double_quote_xml_attribute_values + attr_accessor :message_max_bytesize attr_accessor :passive attr_reader :protocol_binding attr_accessor :attributes_index @@ -264,6 +265,7 @@ def get_binding(value) :idp_cert_fingerprint_algorithm => XMLSecurity::Document::SHA1, :compress_request => true, :compress_response => true, + :message_max_bytesize => 250000, :soft => true, :double_quote_xml_attribute_values => false, :security => { diff --git a/lib/onelogin/ruby-saml/slo_logoutrequest.rb b/lib/onelogin/ruby-saml/slo_logoutrequest.rb index a5c405e7c..9d34a961e 100644 --- a/lib/onelogin/ruby-saml/slo_logoutrequest.rb +++ b/lib/onelogin/ruby-saml/slo_logoutrequest.rb @@ -43,7 +43,7 @@ def initialize(request, options = {}) end end - @request = decode_raw_saml(request) + @request = decode_raw_saml(request, settings) @document = REXML::Document.new(@request) end diff --git a/test/saml_message_test.rb b/test/saml_message_test.rb index 7aa494d38..2ebe3f3c9 100644 --- a/test/saml_message_test.rb +++ b/test/saml_message_test.rb @@ -54,21 +54,33 @@ class RubySamlTest < Minitest::Test end describe "Prevent Zlib bomb attack" do + let(:bomb) { Base64.encode64(Zlib::Deflate.deflate(bomb_data, 9)[2..-5]) } + let(:bomb_data) { bomb_prefix + "A" * (200000 * 1024) + bomb_suffix } + let(:bomb_prefix) { """ + + """ } + let(:bomb_suffix) { """ + ONELOGIN_f92cc1834efc0f73e9c09f482fce80037a6251e7 + """ } + it "raises error when SAML Message exceed the allowed bytes" do - prefix= """ - - """ - suffix= """ - ONELOGIN_f92cc1834efc0f73e9c09f482fce80037a6251e7 - """ - - data = prefix + "A" * (200000 * 1024) + suffix - bomb = Base64.encode64(Zlib::Deflate.deflate(data, 9)[2..-5]) - assert_raises(OneLogin::RubySaml::ValidationError, "Encoded SAML Message exceeds " + OneLogin::RubySaml::SamlMessage::MAX_BYTE_SIZE.to_s + " bytes, so was rejected") do + assert_raises(OneLogin::RubySaml::ValidationError, "Encoded SAML Message exceeds #{OneLogin::RubySaml::Settings::DEFAULTS[:message_max_bytesize]} bytes, so was rejected") do saml_message = OneLogin::RubySaml::SamlMessage.new saml_message.send(:decode_raw_saml, bomb) end end + + describe 'with a custom setting for message_max_bytesize' do + let(:message_max_bytesize) { 100_00 } + let(:settings) { OneLogin::RubySaml::Settings.new(message_max_bytesize: message_max_bytesize) } + + it 'uses the custom setting' do + assert_raises(OneLogin::RubySaml::ValidationError, "Encoded SAML Message exceeds #{message_max_bytesize} bytes, so was rejected") do + saml_message = OneLogin::RubySaml::SamlMessage.new + saml_message.send(:decode_raw_saml, bomb, settings) + end + end + end end end end \ No newline at end of file diff --git a/test/settings_test.rb b/test/settings_test.rb index 37453eb03..188264521 100644 --- a/test/settings_test.rb +++ b/test/settings_test.rb @@ -17,7 +17,7 @@ class SettingsTest < Minitest::Test :idp_attribute_names, :issuer, :assertion_consumer_service_url, :single_logout_service_url, :sp_name_qualifier, :name_identifier_format, :name_identifier_value, :name_identifier_value_requested, :sessionindex, :attributes_index, :passive, :force_authn, - :compress_request, :double_quote_xml_attribute_values, + :compress_request, :double_quote_xml_attribute_values, :message_max_bytesize, :security, :certificate, :private_key, :authn_context, :authn_context_comparison, :authn_context_decl_ref, :assertion_consumer_logout_service_url @@ -89,6 +89,7 @@ class SettingsTest < Minitest::Test :idp_slo_service_url => "http://sso.muda.no/slo", :idp_slo_service_binding => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", :idp_cert_fingerprint => "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00", + :message_max_bytesize => 750000, :valid_until => '2029-04-16T03:35:08.277Z', :name_identifier_format => "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", :attributes_index => 30,