Integrate AXS_SENSITIVE_PARAMETERS functionality with AXES_PASSWORD_FORM_FIELD

This commit is contained in:
Michael O'Connor 2021-04-23 17:06:03 -05:00 committed by Aleksi Häkli
parent f54c4f095b
commit 170dacc112
5 changed files with 53 additions and 44 deletions

View file

@ -123,7 +123,5 @@ settings.AXES_ALLOWED_CORS_ORIGINS = getattr(settings, "AXES_ALLOWED_CORS_ORIGIN
settings.AXES_SENSITIVE_PARAMETERS = getattr(
settings,
"AXES_SENSITIVE_PARAMETERS",
[
"password",
],
[],
)

View file

@ -17,7 +17,6 @@ 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
@ -110,8 +109,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(cleanse_params(request.GET)).replace("\0", "0x00")
post_data = get_query_str(cleanse_params(request.POST)).replace("\0", "0x00")
get_data = get_query_str(request.GET).replace("\0", "0x00")
post_data = get_query_str(request.POST).replace("\0", "0x00")
if self.is_whitelisted(request, credentials):
log.info("AXES: Login failed from whitelisted client %s.", client_str)

View file

@ -277,18 +277,42 @@ def get_client_str(
return client_str
def cleanse_parameters(params: dict) -> dict:
"""
Replace sensitive parameter values in a parameter dict with
a safe placeholder value.
Parameters name ``'password'`` will always be cleansed. Additionally,
parameters named in ``settings.AXES_SENSITIVE_PARAMETERS`` and
``settings.AXES_PASSWORD_FORM_FIELD will be cleansed.
This is used to prevent passwords and similar values from
being logged in cleartext.
"""
sensitive_parameters = ["password"] + settings.AXES_SENSITIVE_PARAMETERS
if settings.AXES_PASSWORD_FORM_FIELD:
sensitive_parameters.append(settings.AXES_PASSWORD_FORM_FIELD)
if sensitive_parameters:
cleansed = params.copy()
for param in sensitive_parameters:
if param in cleansed:
cleansed[param] = "********************"
return cleansed
return params
def get_query_str(query: Type[QueryDict], max_length: int = 1024) -> str:
"""
Turns a query dictionary into an easy-to-read list of key-value pairs.
If a field is called either ``'password'`` or ``settings.AXES_PASSWORD_FORM_FIELD`` it will be excluded.
If a field is called either ``'password'`` or ``settings.AXES_PASSWORD_FORM_FIELD`` or if the fieldname is included
in ``settings.AXES_SENSITIVE_PARAMETERS`` its value will be masked.
The length of the output is limited to max_length to avoid a DoS attack via excessively large payloads.
"""
query_dict = query.copy()
query_dict.pop("password", None)
query_dict.pop(settings.AXES_PASSWORD_FORM_FIELD, None)
query_dict = cleanse_parameters(query.copy())
template = Template("$key=$value")
items = [{"key": k, "value": v} for k, v in query_dict.items()]
@ -476,24 +500,3 @@ 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

View file

@ -94,6 +94,9 @@ The following ``settings.py`` options are available for customizing Axes behavio
Default: ``None``
* ``AXES_PASSWORD_FORM_FIELD``: the name of the form or credentials field that contains your users password.
Default: ``password``
* ``AXES_SENSITIVE_PARAMETERS``: Configures POST and GET parameter values (in addition to the value of
``AXES_PASSWORD_FORM_FIELD``) to mask in login attempt logging.
Default: ``[]``
* ``AXES_NEVER_LOCKOUT_GET``: If ``True``, Axes will never lock out HTTP GET requests.
Default: ``False``
* ``AXES_NEVER_LOCKOUT_WHITELIST``: If ``True``, users can always login from whitelisted IP addresses.
@ -111,8 +114,6 @@ 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:

View file

@ -23,7 +23,7 @@ from axes.helpers import (
is_ip_address_in_whitelist,
is_user_attempt_whitelisted,
toggleable,
cleanse_params,
cleanse_parameters,
)
from axes.models import AccessAttempt
from tests.base import AxesTestCase
@ -687,28 +687,36 @@ class AxesLockoutTestCase(AxesTestCase):
class AxesCleanseParamsTestCase(AxesTestCase):
def setUp(self):
self.params = {
self.parameters = {
"username": "test_user",
"password": "test_password",
"other_sensitive_data": "sensitive",
}
def test_cleanse_params(self):
cleansed = cleanse_params(self.params)
def test_cleanse_parameters(self):
cleansed = cleanse_parameters(self.parameters)
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)
def test_cleanse_parameters_override_sensitive(self):
cleansed = cleanse_parameters(self.parameters)
self.assertEqual("test_user", cleansed["username"])
self.assertEqual("test_password", cleansed["password"])
self.assertEqual("********************", 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)
@override_settings(AXES_SENSITIVE_PARAMETERS=["other_sensitive_data"])
@override_settings(AXES_PASSWORD_FORM_FIELD="username")
def test_cleanse_parameters_override_both(self):
cleansed = cleanse_parameters(self.parameters)
self.assertEqual("********************", cleansed["username"])
self.assertEqual("********************", cleansed["password"])
self.assertEqual("********************", cleansed["other_sensitive_data"])
@override_settings(AXES_PASSWORD_FORM_FIELD=None)
def test_cleanse_parameters_override_empty(self):
cleansed = cleanse_parameters(self.parameters)
self.assertEqual("test_user", cleansed["username"])
self.assertEqual("test_password", cleansed["password"])
self.assertEqual("********************", cleansed["password"])
self.assertEqual("sensitive", cleansed["other_sensitive_data"])