Skip to content

Commit 6d6bc9c

Browse files
Make replay attacks result in 403
Attempting to replay a SAMLResponse makes PySAML raise an UnsolicitedResponse exception. That exception should be caught to properly display an error instead of generating a server error.
1 parent 70209ed commit 6d6bc9c

File tree

3 files changed

+39
-1
lines changed

3 files changed

+39
-1
lines changed

CHANGES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ Changes
33

44
UNRELEASED
55
----------
6+
- A 403 (permission denied) is now raised if a SAMLResponse is replayed, instead of 500.
67
- Log when fields are missing in a SAML response.
78
- Log when attribute_mapping maps to nonexistent User fields.
89
- Dropped compatibility for Python < 2.7 and Django < 1.8.

djangosaml2/tests/__init__.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,37 @@ def test_assertion_consumer_service(self):
257257
self.assertEqual(url.path, settings.LOGIN_REDIRECT_URL)
258258
self.assertEqual(force_text(new_user.id), self.client.session[SESSION_KEY])
259259

260+
def test_assertion_consumer_service_no_session(self):
261+
settings.SAML_CONFIG = conf.create_conf(
262+
sp_host='sp.example.com',
263+
idp_hosts=['idp.example.com'],
264+
metadata_file='remote_metadata_one_idp.xml',
265+
)
266+
267+
# session_id should start with a letter since it is a NCName
268+
session_id = "a0123456789abcdef0123456789abcdef"
269+
came_from = '/another-view/'
270+
self.add_outstanding_query(session_id, came_from)
271+
272+
# Authentication is confirmed.
273+
saml_response = auth_response(session_id, 'student')
274+
response = self.client.post(reverse('saml2_acs'), {
275+
'SAMLResponse': self.b64_for_post(saml_response),
276+
'RelayState': came_from,
277+
})
278+
self.assertEqual(response.status_code, 302)
279+
location = response['Location']
280+
url = urlparse(location)
281+
self.assertEqual(url.path, came_from)
282+
283+
# Session should no longer be in outstanding queries.
284+
saml_response = auth_response(session_id, 'student')
285+
response = self.client.post(reverse('saml2_acs'), {
286+
'SAMLResponse': self.b64_for_post(saml_response),
287+
'RelayState': came_from,
288+
})
289+
self.assertEqual(response.status_code, 403)
290+
260291
def test_missing_param_to_assertion_consumer_service_request(self):
261292
# Send request without SAML2Response parameter
262293
response = self.client.post(reverse('saml2_acs'))

djangosaml2/views.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@
4545
from saml2.ident import code, decode
4646
from saml2.sigver import MissingKey
4747
from saml2.s_utils import UnsupportedBinding
48-
from saml2.response import StatusError, StatusAuthnFailed, SignatureError, StatusRequestDenied
48+
from saml2.response import (
49+
StatusError, StatusAuthnFailed, SignatureError, StatusRequestDenied,
50+
UnsolicitedResponse,
51+
)
4952
from saml2.validate import ResponseLifetimeExceed, ToEarly
5053
from saml2.xmldsig import SIG_RSA_SHA1, SIG_RSA_SHA256 # support for SHA1 is required by spec
5154

@@ -287,6 +290,9 @@ def assertion_consumer_service(request,
287290
except MissingKey:
288291
logger.exception("SAML Identity Provider is not configured correctly: certificate key is missing!")
289292
return fail_acs_response(request)
293+
except UnsolicitedResponse:
294+
logger.exception("Received SAMLResponse when no request has been made.")
295+
return fail_acs_response(request)
290296

291297
if response is None:
292298
logger.warning("Invalid SAML Assertion received (unknown error).")

0 commit comments

Comments
 (0)