The convenience of Single Sign On (SSO) protocols are becoming increasingly popular for IT folks, and for good reason: it makes life so much easier for IT pros and business users to get secure access to day-to-day software and web tools while maintaining the integrity of their password systems. With LogMeIn’s recent acquisition of Meldium, it’s more clear than ever that SSO is growing and becoming a more standardized part of the IT manager’s toolkit. This means security is becoming a greater concern as well as more people rely on SSO as a critical infrastructure piece.
We learned this firsthand a few months ago when our friends, German security researchers Vladislav Mladenov, Julian Krautwald, Florian Feldmann and Christian Mainka, alerted us to a common flaw of the verification of SSO logins related to the popular SAML standard that affected us directly, in addition to a number of other well-known and trusted SSO providers (whom we notified directly of the potential security hole). Anyone following the instructions in this common Ruby toolkit to implement SAML for SSO would experience the same security issue.
While we had no indication that the security flaw was being exploited we came up with a fix quickly because we take security seriously, and we highly recommend anyone using SAML do the same. How it works, in a nutshell: this particular flaw could in theory allow a malicious service provider (MalSP) access to your Panorama9 login if you use the same SAML identity provider (IdP) to log into both MalSP and Panorama9.
Below are step-by-step instructions on how we implemented a fix. We’d love to hear your thoughts or feedback on improving this workflow in the comments! Stay secure, everyone.
How-To address the Single Sign On security hole
First step is to validate the InResponseTo ID received by your application. This will ensure that your application was actually contacted at the URL you announced as the SAML initialization point. The SAML documentation has this initialization example:
def init request = OneLogin::RubySaml::Authrequest.new redirect_to(request.create(saml_settings)) end
This will redirect the users browser back to the Identity Provider, but is missing that you need to remember the ‘request identifier’. It’s an ID generated by your application and will be included in the reply your application later receives during the consumption step. Example how to save the ID during the initialization:
def init request = OneLogin::RubySaml::Authrequest.new request_doc_xml = request.create_authentication_xml_doc(saml_settings) SAMLCache.new(:time_stamp => Time.now.to_i, :request_identifier => request_doc_xml.elements.attributes["ID"].to_s).save redirect_to(auth_to_uri(request_doc_xml, saml_settings)) end
During the consumption step your application should validate that the included ID matches a recent ID. The SAML documentation gives this example, but is missing the validation process:
def consume response = OneLogin::RubySaml::Response.new(params[:SAMLResponse]) response.settings = saml_settings if response.is_valid? && user = current_account.users.find_by_email(response.name_id) authorize_success(user) else authorize_failure(user) end end
This could be your enhanced consume function:
def consume response = OneLogin::RubySaml::Response.new(params[:SAMLResponse]) response.settings = saml_settings #valid login response from Identity Provider? if !response.is_valid? && user = current_account.users.find_by_email(response.name_id) authorize_failure(user) end #valid InResponseTo ID request_identifier = response.document.elements.attributes["InResponseTo"] if !SAMLCache.validate_id(request_identifier) authorize_failure(user) end #authorize authorize_success(user) end
This will protect against MalSP reusing a consume reply sent to MalSP’s application. This is good but it isn’t enough. A sneaky MalSP could get a valid ID from your application and then use that same ID when communicating with an user trying to access MalSP’s application. Once MalSP receives the consume reply it can also be sent to your application which will then recognize the ID as properly issued and assume that it is valid.
To foil this attack your application must also check that the consume reply is indeed intended for your application – you should validate the SAML audience element and recipient attribute. This way you can be sure that the received consume reply was issued by the Identity Provider for your application. Your already enhanced consume function should also do something like this:
#extract audience and recipient from response audience = response.document.elements.elements['saml:Assertion'].elements['saml:Conditions'].elements['saml:AudienceRestriction'].elements['saml:Audience'].to_s recipient = response.document.elements.elements['saml:Assertion'].elements['saml:Subject'].elements['saml:SubjectConfirmation'].elements['saml:SubjectConfirmationData'].attributes["Recipient"].to_s #validate them if audience !~ /YOUR_DOMAIN/ || recipient !~ /^URL_TO_CONSUME/ authorize_failure(user) end
The SAML audience element (
YOUR_DOMAIN in the above example, e.g.
panorama9.com) and recipient attribute (
URL_TO_CONSUME in the above example, e.g.
https://dashboard.panorama9.com/saml/consume )values needs to be set by the Identity Provider. In our experience this is not always the case but we hope we’ve explained why this is vital to your security. We hope this guide helps you address the Single Sign On security hole. Feel free to leave a comment if you have additional questions.