Api Key Exposure in Django with Saml
Api Key Exposure in Django with Saml — how this specific combination creates or exposes the vulnerability
When Django applications consume SAML-based identity providers, developers often handle SAML assertions containing sensitive attributes that may include or be used to derive secrets intended for internal services. If these secrets—such as API keys—are written to logs, exposed through debug pages, passed in URLs, or stored in session attributes without care, they can be exfiltrated.
Django’s SAML integration typically relies on third‑party libraries (e.g., python3-saml) that parse incoming assertions. Misconfigured settings can inadvertently expose sensitive data. For example, setting DEBUG = True in production may render detailed error pages that include assertion contents, including embedded keys or tokens. Similarly, verbose logging of the full SAML response or attributes can capture API keys that were transmitted as attribute statements.
Another vector is improper session handling. If a SAML-authenticated session stores an API key in the session dictionary without encryption or proper scoping, session fixation or session hijack attacks can expose the key. Additionally, if the application passes API keys as query parameters during SAML-related redirects or Single Logout flows, those keys may leak in browser history, server logs, or referrer headers.
SSRF risks also intersect with this space. A SAML endpoint may be used to fetch metadata; if user-controlled URLs are accepted for metadata resolution without strict allowlisting, an attacker can direct the resolver to internal services that return sensitive configuration, including API keys. This compounds exposure when combined with overly permissive attribute selectors that map SAML attributes to internal credentials.
Lastly, without runtime validation of the SAML response’s destination and audience, a malicious IdP can inject crafted attributes containing key-like values, which the Django app may trust and propagate to downstream services, effectively exposing API keys to unauthorized contexts.
Saml-Specific Remediation in Django — concrete code fixes
Remediation centers on strict input validation, secure session management, and avoiding the persistence of secrets in logs or session state. Below are concrete, safe patterns for handling SAML in Django.
1. Secure SAML configuration and response validation
Ensure your SAML configuration explicitly sets the destination and audience constraints so that responses intended for your service are rejected if mismatched.
from onelogin.saml2.auth import OneLogin_Saml2_Auth
def prepare_saml_request(req):
auth = OneLogin_Saml2_Auth(req, custom_base_path='saml/settings/')
auth.login(return_to='https://app.example.com/acs/')
return auth.get_last_request_id()
# settings/saml_settings.json (example)
{
"strict": True,
"debug": False,
"sp": {
"entityId": "https://app.example.com/metadata/",
"assertionConsumerService": {
"url": "https://app.example.com/acs/",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
},
"singleLogoutService": {
"url": "https://app.example.com/sls/",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
}
},
"idp": {
"entityId": "https://idp.example.com/metadata",
"singleSignOnService": {
"url": "https://idp.example.com/sso/saml",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
"x509cert": "MIIC..."
},
"security": {
"nameuid": False,
"wantXMLValidation": True,
"wantAssertionsSigned": True,
"wantMessageSigned": False,
"wantEncryptedAssertions": False,
"rejectUnsolicitedResponsesWithInResponseTo": True,
"destination_url": "https://app.example.com/acs/",
"audience": ["https://app.example.com/metadata/"]
}
}
Using wantAssertionsSigned and destination_url
2. Avoid storing or logging sensitive attributes
Filter SAML attributes before they enter session or logs. Do not persist raw assertion attributes that may contain API keys or secrets.
import logging
logger = logging.getLogger(__name__)
def saml_login_view(request):
req = prepare_saml_request(request)
auth = OneLogin_Saml2_Auth(req)
auth.process_response()
errors = auth.get_errors()
if len(errors) == 0:
attributes = auth.get_attributes()
# Explicitly select safe attributes; exclude keys/secrets
safe_attrs = {
"email": attributes.get('email', [None])[0],
"nameid": auth.get_nameid()
}
# Never log raw attributes
logger.info("SAML login succeeded for email: %s", safe_attrs["email"])
request.session['user_email'] = safe_attrs["email"]
request.session['nameid'] = safe_attrs["nameid"]
# Do NOT store API keys from attributes
return redirect('home')
else:
logger.warning("SAML response error: %s", errors)
return redirect('login?error=invalid')
3. Enforce HTTPS and secure cookies
Protect the session and any redirect flows by enforcing HTTPS and marking cookies as Secure and HttpOnly.
# settings.py
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_SECURE = True
SECURE_SSL_REDIRECT = True
4. Do not pass API keys via query parameters
During SAML redirects or logout, avoid embedding keys in URLs. Use session-based references instead.
# Bad: redirect with key in query
# return redirect(f'https://service.example.com/init?api_key={raw_key}')
# Good: store key server-side, pass a reference
request.session['service_token'] = generate_secure_token()
return redirect('https://service.example.com/init')
5. Limit metadata fetching and use allowlists
If your flow fetches IdP metadata, restrict the source to a known, allowlisted URL and avoid SSRF by not accepting arbitrary URLs.
import requests
from django.conf import settings
ALLOWED_METADATA_URLS = ["https://idp.example.com/metadata"]
def fetch_metadata(url):
if url not in ALLOWED_METADATA_URLS:
raise ValueError("Metadata URL not allowlisted")
resp = requests.get(url, timeout=5, verify=True)
resp.raise_for_status()
return resp.content