Security Assertion Markup Language (SAML) is the XML-based protocol that powers single sign-on for enterprise applications — Salesforce, AWS, Workday, GitHub Enterprise, Okta, and hundreds of others accept SAML assertions to authenticate users. Its prevalence and the complexity of XML digital signature processing have made it one of the most historically exploited protocols in enterprise authentication, with vulnerabilities surfacing regularly despite the protocol being defined in 2005.
This guide covers the attack classes, shows how they work in practice, and explains what secure SAML implementation looks like in Python and Java.
SAML Basics: What the Protocol Does
In a SAML flow:
- A user attempts to access a Service Provider (SP) — the application they want to use.
- The SP redirects the user to the Identity Provider (IdP) — the central authentication service (Okta, Azure AD, ADFS, etc.).
- The IdP authenticates the user and generates a SAML Response — an XML document containing assertions (claims about the user’s identity and attributes).
- The IdP signs the response or assertion with its private key.
- The SP receives the response, verifies the signature against the IdP’s public certificate, and grants access based on the assertions.
The security model depends entirely on the SP correctly validating the XML signature. Failures in that validation are where vulnerabilities live.
Attack Class 1: XML Signature Wrapping (XSW)
XML Signature Wrapping is the most significant class of SAML vulnerability. The core insight: XML Signature validation and XML processing use different mechanisms to identify which element is “the signed element.” An attacker can satisfy the signature check on a legitimate signed element while inserting a different element that the application actually reads for authorization decisions.
SAML responses are signed using XML-DSIG. The signature includes a Reference element that specifies which element was signed, using an ID attribute. When an SP validates the signature, it:
- Finds the element referenced by the signature.
- Verifies the signature over that element’s canonical form.
- Extracts the user identity from the assertion in the response.
The vulnerability: steps 1 and 3 use different code paths. If the SP uses XPath or DOM traversal to find the assertion (step 3) rather than using the same reference resolution as the signature check (step 1), an attacker can insert a second, unsigned assertion into a position where step 3 finds it first.
Minimal XSW Example
Original signed response (simplified):
<samlp:Response>
<Assertion ID="signed-assertion" xmlns:saml="...">
<Subject><NameID>[email protected]</NameID></Subject>
<Signature><!-- Valid signature over #signed-assertion --></Signature>
</Assertion>
</samlp:Response>
Attacker inserts a second assertion before the signed one:
<samlp:Response>
<!-- Unsigned, attacker-controlled assertion in a position the SP reads first -->
<Assertion ID="evil-assertion">
<Subject><NameID>[email protected]</NameID></Subject>
</Assertion>
<!-- Original signed assertion — still valid, satisfies signature check -->
<Assertion ID="signed-assertion">
<Subject><NameID>[email protected]</NameID></Subject>
<Signature><!-- Valid signature --></Signature>
</Assertion>
</samlp:Response>
If the SP’s assertion extraction logic returns the first <Assertion> element in the response, it grants access as [email protected]. The signature check still passes because the legitimate signed assertion is present and valid.
There are eight documented XSW variants (XSW1–XSW8), differing in where the injected element is placed (inside the signed element, in the header, as an extension node, etc.). Different SAML libraries are vulnerable to different subsets.
Attack Class 2: Comment Injection / Namespace Confusion
A subtler attack exploits inconsistencies between how the XML signature canonicalization algorithm processes XML and how the application’s assertion parser processes the same XML.
Comment injection example:
The IdP issues an assertion for [email protected]. An attacker modifies the NameID value by inserting an XML comment:
<NameID>admin<!--injected-->@company.com</NameID>
If the canonicalization algorithm (which the signature was computed over) strips comments, the signed text is [email protected]. If the SAML library’s attribute extractor processes the raw XML and also strips comments, the extracted identity is also [email protected] — but the original assertion was for a different user. This vulnerability affected multiple OneLogin libraries and was exploited in the wild.
Attack Class 3: Signature Exclusion / Missing Signature Validation
The most basic class — many SAML implementations fail to enforce that a signature must be present, or accept an unsigned assertion if the surrounding response is signed (or vice versa):
<!-- Attacker sends an assertion with no Signature element at all -->
<Assertion>
<Subject><NameID>[email protected]</NameID></Subject>
<!-- No Signature element -->
</Assertion>
If the SP’s code path for “assertion has no signature” simply proceeds rather than rejecting, this is a complete authentication bypass.
Vulnerable Python Example
# VULNERABLE: Using python3-saml without enforcing validation settings
from onelogin.saml2.auth import OneLogin_Saml2_Auth
def process_saml_response(request):
auth = OneLogin_Saml2_Auth(request, custom_base_path='/saml/settings')
auth.process_response() # May silently skip signature validation
if auth.is_authenticated():
# auth.get_nameid() might return attacker-controlled value
user_email = auth.get_nameid()
login_user(user_email) # Vulnerable to XSW
Secure Python Implementation
# SECURE: python3-saml with strict validation enforced
from onelogin.saml2.auth import OneLogin_Saml2_Auth
from onelogin.saml2.utils import OneLogin_Saml2_Utils
# settings.json must include:
# {
# "strict": true, ← Required — rejects invalid signatures, wrong audiences, replay attacks
# "security": {
# "wantAssertionsSigned": true, ← Require signature on assertions (not just response wrapper)
# "wantMessagesSigned": false,
# "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
# "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256",
# "rejectDeprecatedAlgorithm": true ← Blocks SHA-1 signature attacks
# }
# }
def process_saml_response(request):
auth = OneLogin_Saml2_Auth(request, custom_base_path='/saml/settings')
auth.process_response()
errors = auth.get_errors()
if errors:
raise ValueError(f"SAML validation failed: {errors}")
if not auth.is_authenticated():
raise ValueError("SAML authentication failed")
# Use get_nameid() only after confirmed validation
user_email = auth.get_nameid()
# Additional application-layer validation
if not user_email.endswith('@yourcompany.com'):
raise ValueError(f"Unexpected NameID domain: {user_email}")
return user_email
Secure Java Implementation (Spring Security SAML)
// Spring Security SAML — secure configuration
@Configuration
@EnableWebSecurity
public class SAMLSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public SAMLContextProvider contextProvider() {
SAMLContextProviderImpl contextProvider = new SAMLContextProviderImpl();
// Enforce HTTPS for all SAML endpoints
contextProvider.setStorageFactory(new EmptyStorageFactory());
return contextProvider;
}
@Bean
public WebSSOProfileConsumer webSSOprofileConsumer() {
WebSSOProfileConsumerImpl consumer = new WebSSOProfileConsumerImpl();
// Reject assertions with no valid signature
consumer.setResponseSkew(60); // Max 60 seconds clock skew
consumer.setMaxAuthenticationAge(7200); // 2-hour session max
consumer.setIncludeAllAttributes(true);
return consumer;
}
@Bean
public SAMLProcessor processor() {
// Only accept POST binding — avoids redirect binding signature issues
Collection<SAMLBinding> bindings = new ArrayList<>();
bindings.add(httpPostBinding());
return new SAMLProcessorImpl(bindings);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/saml/**").permitAll()
.anyRequest().authenticated()
.and()
.apply(saml())
.userDetailsService(samlUserDetailsService())
.ssoDefaultSuccessURL("/dashboard")
.failureRedirectURL("/error");
}
}
Validating Assertions Correctly: What to Check
A complete SAML assertion validation must verify all of the following:
# Checklist implemented in python3-saml strict mode
# Verify each of these manually if implementing your own validation:
def validate_saml_assertion(assertion, sp_config, idp_cert):
checks = [
# 1. Signature is present on the assertion (not just the response)
assert_has_signature(assertion),
# 2. Signature is valid against the IdP's known certificate
verify_signature(assertion, idp_cert),
# 3. Issuer matches the expected IdP entity ID
verify_issuer(assertion, sp_config['idp_entity_id']),
# 4. Audience restriction matches this SP's entity ID
verify_audience(assertion, sp_config['sp_entity_id']),
# 5. NotBefore and NotOnOrAfter timestamps are within acceptable window
verify_time_validity(assertion, skew_seconds=60),
# 6. InResponseTo matches the AuthnRequest ID (prevents replay attacks)
verify_in_response_to(assertion, sp_config['authn_request_id']),
# 7. The subject confirmation method is bearer (for web SSO)
verify_subject_confirmation(assertion),
# 8. Signature algorithm is SHA-256 or better (reject SHA-1)
verify_signature_algorithm(assertion, min_bits=256),
]
if not all(checks):
raise SAMLValidationError("One or more assertion checks failed")
Testing Your SAML Implementation
Use SAMLRaider (Burp Suite extension) or evilsaml to test your SP for XSW vulnerabilities. The tool automates the eight XSW variants and reports which, if any, bypass signature validation:
# Test with saml-raider via command line (Burp headless):
java -jar saml-raider.jar \
--url https://yourapp.com/saml/acs \
--saml-response /tmp/captured-response.xml \
--attack XSW1,XSW2,XSW3,XSW4,XSW5,XSW6,XSW7,XSW8 \
--attacker-nameid [email protected]
Any attack that achieves a 200 or redirect to a logged-in state indicates a signature wrapping vulnerability in your SP implementation.