diff --git a/axes/conf.py b/axes/conf.py index 0ae3178..9e8e41f 100644 --- a/axes/conf.py +++ b/axes/conf.py @@ -118,3 +118,12 @@ settings.AXES_META_PRECEDENCE_ORDER = getattr( # set CORS allowed origins when calling authentication over ajax settings.AXES_ALLOWED_CORS_ORIGINS = getattr(settings, "AXES_ALLOWED_CORS_ORIGINS", "*") + +# set the list of sensitive parameters to cleanse from get/post data before logging +settings.AXES_SENSITIVE_PARAMETERS = getattr( + settings, + "AXES_SENSITIVE_PARAMETERS", + [ + "password", + ], +) diff --git a/axes/handlers/database.py b/axes/handlers/database.py index 187a089..8cc9a61 100644 --- a/axes/handlers/database.py +++ b/axes/handlers/database.py @@ -17,6 +17,7 @@ from axes.helpers import ( get_credentials, get_failure_limit, get_query_str, + cleanse_params, ) from axes.models import AccessLog, AccessAttempt from axes.signals import user_locked_out @@ -109,8 +110,8 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler): ) # This replaces null byte chars that crash saving failures, meaning an attacker doesn't get locked out. - get_data = get_query_str(request.GET).replace("\0", "0x00") - post_data = get_query_str(request.POST).replace("\0", "0x00") + get_data = get_query_str(cleanse_params(request.GET)).replace("\0", "0x00") + post_data = get_query_str(cleanse_params(request.POST)).replace("\0", "0x00") if self.is_whitelisted(request, credentials): log.info("AXES: Login failed from whitelisted client %s.", client_str) diff --git a/axes/helpers.py b/axes/helpers.py index f0f35f3..920a40c 100644 --- a/axes/helpers.py +++ b/axes/helpers.py @@ -476,3 +476,24 @@ def toggleable(func) -> Callable: return func(*args, **kwargs) return inner + + +def cleanse_params(params: dict) -> dict: + """ + Replace sensitive parameter values in a parameter dict with + a safe placeholder value. + + Parameters to be cleansed are named in + ``settings.AXES_SENSITIVE_PARAMETERS``. If this setting is + empty, no parameters will be replaced. + + This is used to prevent passwords and similar values from + being logged in cleartext. + """ + if settings.AXES_SENSITIVE_PARAMETERS: + cleansed = params.copy() + for param in settings.AXES_SENSITIVE_PARAMETERS: + if param in cleansed: + cleansed[param] = "********************" + return cleansed + return params diff --git a/docs/4_configuration.rst b/docs/4_configuration.rst index ef2e912..c7cb5d7 100644 --- a/docs/4_configuration.rst +++ b/docs/4_configuration.rst @@ -111,6 +111,8 @@ The following ``settings.py`` options are available for customizing Axes behavio Default: ``False`` * ``AXES_ALLOWED_CORS_ORIGINS``: Configures lockout response CORS headers for XHR requests. Default: ``*`` +* ``AXES_SENSITIVE_PARAMS``: Configures POST and GET parameter values to mask in login attempt logging. + Default: ``['password',]`` The configuration option precedences for the access attempt monitoring are: diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 36cd141..b856855 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -23,6 +23,7 @@ from axes.helpers import ( is_ip_address_in_whitelist, is_user_attempt_whitelisted, toggleable, + cleanse_params, ) from axes.models import AccessAttempt from tests.base import AxesTestCase @@ -602,6 +603,7 @@ class LockoutResponseTestCase(AxesTestCase): response = get_lockout_response(request=self.request) self.assertEqual(type(response), HttpResponse) + def mock_get_cool_off_str(): return timedelta(seconds=30) @@ -681,3 +683,32 @@ class AxesLockoutTestCase(AxesTestCase): def test_get_lockout_response_override_invalid(self): with self.assertRaises(TypeError): get_lockout_response(self.request, self.credentials) + + +class AxesCleanseParamsTestCase(AxesTestCase): + def setUp(self): + self.params = { + "username": "test_user", + "password": "test_password", + "other_sensitive_data": "sensitive", + } + + def test_cleanse_params(self): + cleansed = cleanse_params(self.params) + self.assertEqual("test_user", cleansed["username"]) + self.assertEqual("********************", cleansed["password"]) + self.assertEqual("sensitive", cleansed["other_sensitive_data"]) + + @override_settings(AXES_SENSITIVE_PARAMETERS=["other_sensitive_data"]) + def test_cleanse_params_override(self): + cleansed = cleanse_params(self.params) + self.assertEqual("test_user", cleansed["username"]) + self.assertEqual("test_password", cleansed["password"]) + self.assertEqual("********************", cleansed["other_sensitive_data"]) + + @override_settings(AXES_SENSITIVE_PARAMETERS=[]) + def test_cleanse_params_override_empty(self): + cleansed = cleanse_params(self.params) + self.assertEqual("test_user", cleansed["username"]) + self.assertEqual("test_password", cleansed["password"]) + self.assertEqual("sensitive", cleansed["other_sensitive_data"])