https://joonas.fi/2021/08/saml-is-insecure-by-design/ * Home * About * Contact * * * Previous post Next post Back to top Share post * * * * * * * * * * * * What is SAML? * Why should I care? * Why is SAML insecure? * Why is signing computed values dangerous? * The SAML vulnerability in practice * Why is SAML this way? * Vulnerability mitigation * How could SAML have been designed better? * More SAML weirdness * Why is SAML used if it sucks? * Action * Ignorance is bliss * Additional reading SAML is insecure by design 2021-08-03 infosec What is SAML? Security Assertion Markup Language (SAML) is an open standard for exchanging authentication and authorization data between parties. Source: Wikipedia SAML is often used for single-sign on ("Sign in with Google", "Sign in with Twitter" etc.). It means when you want to log in to example.com, example.com can trust & use an external authentication provider to assert the user's identity for you. SAML is about communicating these authentication & identity details across organization boundaries (web domains). [saml-overv] Why should I care? SAML is used in so many places, it probably affects your security too. SAML has recently had catastrophic vulnerabilities with a really large impact. For example, if I understood correctly (I probably did, since the security researcher retweeted my reaction) the Finnish tax authority, most government services and health record systems were vulnerable in such a way that an attacker could have gone on to snoop people's tax returns, health records and basically anything government-related that is available online. It's been largely ignored by the media, perhaps because the vulnerabilities weren't taken advantage of (or instances of such weren't detected). Why is SAML insecure? SAML uses signatures based on computed values. The practice is inherently insecure and thus SAML as a design is insecure. Why is signing computed values dangerous? In summary: once you base your security on some computed property, you can now exploit any flaws, differences or ambiguity in this computation. The more complex the computation is, the more dangerous it gets. SAML signature computation is pretty fucking complex. But let's move on to explain the concept. Let's take a pseudo identity document (actual SAML is XML though): $ cat assertion.json { "signed_in_user": "Joonas" } We can sign^1 the above file just as a bunch of bytes: $ cat assertion.json | sha1sum e58dc03a7491f9e5fb2ed664b23d826489c42cc5 Now if we change the file just a little (I added space before the {). We notice that the signature changes: $ cat assertion.json { "signed_in_user": "Joonas" } $ cat assertion.json | sha1sum 0bc80a9ee02f611b70319c9fe12b7e504107354a This is a very good property, because ideally we want any changes (even those considered meaningless at JSON level) to the security-critical document (which SAML is) to produce different signatures. This property is known as non-malleability. Malleability generic definition: the quality of something that can be shaped into something else without breaking, like the malleability of clay. Us signing the document as a raw byte blob makes this non-malleable, i.e. it can't be shaped without breaking it. That's a desired behaviour in information security. SAML is malleable because its signatures are based on computed values: Signature Example Raw content is Security over malleable Raw bytes File or message raw No content Computed Parsed XML tree content Yes values To explain by example, let's get back to the JSON example. We'll use jq (a JSON transformation utility) to compute something from inside our document: $ cat assertion.json { "signed_in_user": "Joonas" } $ cat assertion.json | jq . { "signed_in_user": "Joonas" } (jq . means just re-print the whole document) Notice how piping the file through jq removed the space? That's because at JSON level the space is not important. At first sight this doesn't seem interesting, but we're heading to danger zone and fast. [welcome-to] Let's sign the computed value: $ cat assertion.json | jq . | sha1sum e58dc03a7491f9e5fb2ed664b23d826489c42cc5 Even though the file still has the space modification, the signature now matches the original signature (from the file that didn't have the space added). Why's this dangerous? Let's change the file again: $ cat assertion.json { "signed_in_user": "EvilAttacker", "signed_in_user": "Joonas" } $ cat assertion.json | jq . | sha1sum e58dc03a7491f9e5fb2ed664b23d826489c42cc5 # the above is because: $ cat assertion.json | jq . { "signed_in_user": "Joonas" } The signature still matches the original file. This is because duplicate keys are valid JSON, removed upon processing and most JSON implementations let the last key win. Now what happens if you have two different pieces of code that process the SAML document and they have different interpretations/ parser behaviour regarding JSON duplicate keys (= message semantic content)? [saml-theor] An attacker asked the identity provider to sign an assertion for him, but due to SAML malleability he was able to attack parser differences and tamper the document to still be valid for signature validation but access data for a different user. Now I have hopefully explained how malleability and basing signatures on computed / interpreted content is dangerous. The SAML vulnerability in practice It is not as straightforward as our JSON example what happened with these SAML vulnerabilities, but this illustrates the principle of these vulnerabilities and their root cause: signing computed values and malleability. The latest vulnerabilities were due to XML round-trip instability (see heading "What an XML round-trip vulnerability looks like"). In summary the vulnerability arises from when parsing XML -> writing XML produces semantically different document, i.e. encode(decode (xmlDocument)) != xmlDocument). I'm not 100 % sure but I think since the SAML signature validation needs a XML write step, it went something like this: [saml-round] The above would not be an an attack vector if SAML content-to-be-signed was non-malleable, i.e. any change after the identity provider signs the document would be detected as a signature violation. Why is SAML this way? Let's assume in good faith that the SAML designers knew non-malleability is a good property to have and let's try to guess why they still ended up with a malleable design. So, let's sign something. When one signs something, one get a signature as output: sign(contentToSign, signingKey) -> signature. For the signature to be useful, you need to transport the signature along with contentToSign so that when a consumer reads contentToSign they can verify it with the signature. Sending this alone would have been easy to keep non-malleable: contentToSign But signature is missing. SAML designers probably didn't want to transport the SAML document and its signature separately (the signature possibly in a HTTP header or URL parameter), so for convenience they wanted to embed it in the same XML document: samlDocument +-- contentToSign +-- signature To be more technically correct, it gets even more YOLO than that. The signature is stored under contentToSign, so upon the validation process the signature needs to be ignored (again more dangerous complexity) to not actually include it in contentToSign which would make it an impossible recursive problem: samlDocument +-- contentToSign +-- signature But let's imagine the previous simpler case where the signature was not stored inside contentToSign and get back to the question if we could've made signature validation byte-based! The problem is that it is really hard to extract the bytes belonging to contentToSign from inside the XML message. XML parser APIs to my knowledge don't support this use case. Even if some would, for SAML to be useful they had to cater to what most XML parser implementations support. => When you have samlDocument and you'd want to access its sub-tree contentToSign, you only get XML-level access there, so SAML designers probably didn't think much of it, went [?] and said "let's sign XML-level data then". Signing output of an XML parser is really hard, because you're trying to keep signature input stable from XML parsing output that has parser differences from XML library to library and from language to language. So that's why we have XML dsig which has rules for e.g. sorting XML attributes in some clusterfuck order in order for SAML implementations to reach some kind of stable consensus on which byte sequence to validate the signature against. In the end we always need to match on bytes anyway. This craziness is known as canonicalization and it transforms something like this this: mooo Into bytes like this (so signature input is stable): mooo (This is just an example I invented, I'm not sure which rules actually exist but here are some examples.) Summary: XML sub-trees are hard to sign/validate and there's some horrible things to enable that and as empirical evidence shows, it's a security nightmare. I'm willing to go on record and say that everything using approaches like these is broken and should be considered insecure. Vulnerability mitigation With Go's vulnerability they had to fix the round-trip instability in Go's XML stack, and also as a safety precaution include round-trip stability validation before actually processing the XML. To recap, instead of validating signature from a bunch of bytes, for SAML signature validation we need: * Round-trip stability validation (= XML parsing + encoding) * XML parsing (again) * XML canonicalization (XML dsig, which is encoding again but with specific complex rules and transforms) If that sounds complex to you, it's because it is. The more complex something is, the more likely it is to have bugs and security issues. How could SAML have been designed better? I'm an amateur, so take my idea with a grain of salt, but let's try. (Note: this post is all pseudo code - it's not real SAML. Here's a real example if you're interested.) Instead of doing something like this: e58dc03a7491f9e5fb2ed664b23d826489c42cc5 Joonas (Which we established is difficult to sign/verify correctly and securely.) Take the and serialize its sub-tree into bytes and store it as base64 or similar, so we can transport it as bytes and only XML-parse it once the signature has been verified: e58dc03a7491f9e5fb2ed664b23d826489c42cc5 PEFzc2VydGlvbj48VXNlcklkPkpvb25hczwvVXNlcklkPjwvQXNzZXJ0aW9uPgo= I don't understand much about XML so there may be even prettier ways to transport strings or byte data, but this should be enough to make a point. This way they could've kept the property of everything being inside the one XML document - but you just need to XML-parse twice: 1. First the outer document, then validate the signature against the byte blob 2. If the signature matches, only then parse the inner validated document Sure, purists may argue that storing XML inside XML as a string or bytes is ugly (and I agree with you), but look what we achieved.. The tradeoff is worth it - everything inside SAMLContentToSign is now non-malleable and you don't need to parse security-critical data before it's validated as coming from a trusted source. And we don't need the vomit that is "XML dsig". More SAML weirdness SAML requires you to support use cases where the root of the XML document is unsigned, i.e. you only sign the assertion elements. What is the purpose of allowing attacker-controlled data? You need additional code to discard the unsafe data in these cases anyway because it'd be a catastrophe if you'd end up using it. Why is SAML used if it sucks? I don't know. I'm not aware of a better standard - although I don't know the space well. OAuth2 exists but is geared towards getting authorization to resources, so it's not an authentication / identity protocol per se. More on the differences. OpenID Connect is also a thing. My guess is also that once a standard gains traction, it's hard to migrate to a better option even if one is available, since the previous option already has critical mass (think Whatsapp vs. Signal). Action Let's get rid of SAML. [?] Some experts seem to recommend OAuth2 or OpenID Connect: * https://twitter.com/pquerna/status/1338517755352387584 * https://github.com/dexidp/dex/discussions/1884 If a vendor is offering you SAML, ask for alternatives. Ignorance is bliss It is my experience that the more you learn about any subject, the more you realize it's all held together by bubblegum and duct tape. It's honestly pretty anxiety inducing. [duct-tape-] When I was researching about this subject, I also noticed that the Finnish government websites' security relies on a single-sign-on component implemented in JavaScript (not even TypeScript) which: * Casually parses security-critical certificates with string replaces. * Mixes Node-style callbacks and explicit Promise usage, i.e. has different flow control styles. * It's only one forgotten return away from catastrophic flow control bug where execution accidentally flows to processValidlySignedPostRequest despite signature validation error. + But that's what you get when you implement security-critical software with a language where flow control is not a language feature but a library feature built on top of the language. + TypeScript would at least have given proper async/await flow control with the compiler noticing most of the bugs. + Update: great news, the upstream project the Finnish gov't fork is based on, had recently been migrated to TypeScript. Additional reading * https://mattermost.com/blog/ securing-xml-implementations-across-the-web/ * https://twitter.com/pquerna/status/1349234347266564096 (pquerna has authored security-related open source like a TOTP library) * https://github.com/dexidp/dex/discussions/1884 (ericchiang is a security engineer and a major contributor to Dex) * https://twitter.com/joonas_fi/status/1339205267637100546 * https://www.imperialviolet.org/2014/09/26/pkcs1.html - PKCS#1 has had a lot of issues resulting from parsing overly-flexible structures first, then validating things based on the computed result. "parsing is dangerous". --------------------------------------------------------------------- 1. sha1sum is not at all a good signing function but it works to demonstrate the principle. -[?] --------------------------------------------------------------------- signature Thanks for reading! If you like my writing, consider following me on Twitter. Stay updated on my blog posts & projects - sign up for my newsletter. No spam, unsubscribe any time. RSS also available. --------------------------------------------------------------------- Please enable JavaScript to view the comments powered by Disqus. comments powered by Disqus * Home * About * Contact * What is SAML? * Why should I care? * Why is SAML insecure? * Why is signing computed values dangerous? * The SAML vulnerability in practice * Why is SAML this way? * Vulnerability mitigation * How could SAML have been designed better? * More SAML weirdness * Why is SAML used if it sucks? * Action * Ignorance is bliss * Additional reading * * * * * * * * * * * Menu TOC share Top Copyright (c) 2021 joonas.fi * Home * About * Contact