mirror of
https://github.com/ae-utbm/sith.git
synced 2025-06-27 05:35:16 +00:00
fix: enumeration attack vector on login form
This commit is contained in:
parent
a7f4630d13
commit
02ef8fdb88
@ -26,9 +26,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form method="post" action="{{ url('core:login') }}">
|
<form method="post" action="{{ url('core:login') }}" id="login-form">
|
||||||
{% if form.errors %}
|
{% if form.errors %}
|
||||||
<p class="alert alert-red">{% trans %}Your username and password didn't match. Please try again.{% endtrans %}</p>
|
<p class="alert alert-red">
|
||||||
|
{% trans %}Your credentials didn't match. Please try again.{% endtrans %}
|
||||||
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
@ -38,6 +38,7 @@ from core.markdown import markdown
|
|||||||
from core.models import AnonymousUser, Group, Page, User
|
from core.models import AnonymousUser, Group, Page, User
|
||||||
from core.utils import get_semester_code, get_start_of_semester
|
from core.utils import get_semester_code, get_start_of_semester
|
||||||
from core.views import AllowFragment
|
from core.views import AllowFragment
|
||||||
|
from counter.models import Customer
|
||||||
from sith import settings
|
from sith import settings
|
||||||
|
|
||||||
|
|
||||||
@ -151,24 +152,44 @@ class TestUserLogin:
|
|||||||
def user(self) -> User:
|
def user(self) -> User:
|
||||||
return baker.make(User, password=make_password("plop"))
|
return baker.make(User, password=make_password("plop"))
|
||||||
|
|
||||||
def test_login_fail(self, client, user):
|
@pytest.mark.parametrize(
|
||||||
|
"identifier_getter",
|
||||||
|
[
|
||||||
|
lambda user: user.username,
|
||||||
|
lambda user: user.email,
|
||||||
|
lambda user: Customer.get_or_create(user)[0].account_id,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_login_fail(self, client, user, identifier_getter):
|
||||||
"""Should not login a user correctly."""
|
"""Should not login a user correctly."""
|
||||||
|
identifier = identifier_getter(user)
|
||||||
response = client.post(
|
response = client.post(
|
||||||
reverse("core:login"),
|
reverse("core:login"),
|
||||||
{"username": user.username, "password": "wrong-password"},
|
{"username": identifier, "password": "wrong-password"},
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert (
|
|
||||||
'<p class="alert alert-red">Votre nom d\'utilisateur '
|
|
||||||
"et votre mot de passe ne correspondent pas. Merci de réessayer.</p>"
|
|
||||||
) in response.text
|
|
||||||
assert response.wsgi_request.user.is_anonymous
|
assert response.wsgi_request.user.is_anonymous
|
||||||
|
soup = BeautifulSoup(response.text, "lxml")
|
||||||
|
form = soup.find(id="login-form")
|
||||||
|
assert (
|
||||||
|
form.find(class_="alert alert-red").get_text(strip=True)
|
||||||
|
== "Vos identifiants ne correspondent pas. Veuillez réessayer."
|
||||||
|
)
|
||||||
|
assert form.find("input", attrs={"name": "username"}).get("value") == identifier
|
||||||
|
|
||||||
def test_login_success(self, client, user):
|
@pytest.mark.parametrize(
|
||||||
|
"identifier_getter",
|
||||||
|
[
|
||||||
|
lambda user: user.username,
|
||||||
|
lambda user: user.email,
|
||||||
|
lambda user: Customer.get_or_create(user)[0].account_id,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_login_success(self, client, user, identifier_getter):
|
||||||
"""Should login a user correctly."""
|
"""Should login a user correctly."""
|
||||||
response = client.post(
|
response = client.post(
|
||||||
reverse("core:login"),
|
reverse("core:login"),
|
||||||
{"username": user.username, "password": "plop"},
|
{"username": identifier_getter(user), "password": "plop"},
|
||||||
)
|
)
|
||||||
assertRedirects(response, reverse("core:index"))
|
assertRedirects(response, reverse("core:index"))
|
||||||
assert response.wsgi_request.user == user
|
assert response.wsgi_request.user == user
|
||||||
|
@ -132,29 +132,31 @@ class FutureDateTimeField(forms.DateTimeField):
|
|||||||
|
|
||||||
class LoginForm(AuthenticationForm):
|
class LoginForm(AuthenticationForm):
|
||||||
def __init__(self, *arg, **kwargs):
|
def __init__(self, *arg, **kwargs):
|
||||||
if "data" in kwargs:
|
|
||||||
from counter.models import Customer
|
|
||||||
|
|
||||||
data = kwargs["data"].copy()
|
|
||||||
account_code = re.compile(r"^[0-9]+[A-Za-z]$")
|
|
||||||
try:
|
|
||||||
if account_code.match(data["username"]):
|
|
||||||
user = (
|
|
||||||
Customer.objects.filter(account_id__iexact=data["username"])
|
|
||||||
.first()
|
|
||||||
.user
|
|
||||||
)
|
|
||||||
elif "@" in data["username"]:
|
|
||||||
user = User.objects.filter(email__iexact=data["username"]).first()
|
|
||||||
else:
|
|
||||||
user = User.objects.filter(username=data["username"]).first()
|
|
||||||
data["username"] = user.username
|
|
||||||
except: # noqa E722 I don't know what error is supposed to be raised here
|
|
||||||
pass
|
|
||||||
kwargs["data"] = data
|
|
||||||
super().__init__(*arg, **kwargs)
|
super().__init__(*arg, **kwargs)
|
||||||
self.fields["username"].label = _("Username, email, or account number")
|
self.fields["username"].label = _("Username, email, or account number")
|
||||||
|
|
||||||
|
def clean_username(self):
|
||||||
|
identifier: str = self.cleaned_data["username"]
|
||||||
|
account_code = re.compile(r"^[0-9]+[A-Za-z]$")
|
||||||
|
if account_code.match(identifier):
|
||||||
|
qs_filter = "customer__account_id__iexact"
|
||||||
|
elif identifier.count("@") == 1:
|
||||||
|
qs_filter = "email"
|
||||||
|
else:
|
||||||
|
qs_filter = None
|
||||||
|
if qs_filter:
|
||||||
|
# if the user gave an email or an account code instead of
|
||||||
|
# a username, retrieve and return the corresponding username.
|
||||||
|
# If there is no username, return an empty string, so that
|
||||||
|
# Django will properly handle the error when failing the authentication
|
||||||
|
identifier = (
|
||||||
|
User.objects.filter(**{qs_filter: identifier})
|
||||||
|
.values_list("username", flat=True)
|
||||||
|
.first()
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
return identifier
|
||||||
|
|
||||||
|
|
||||||
class RegisteringForm(UserCreationForm):
|
class RegisteringForm(UserCreationForm):
|
||||||
error_css_class = "error"
|
error_css_class = "error"
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-06-16 14:54+0200\n"
|
"POT-Creation-Date: 2025-06-25 16:29+0200\n"
|
||||||
"PO-Revision-Date: 2016-07-18\n"
|
"PO-Revision-Date: 2016-07-18\n"
|
||||||
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
||||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||||
@ -2015,10 +2015,8 @@ msgid "Please login or create an account to see this page."
|
|||||||
msgstr "Merci de vous identifier ou de créer un compte pour voir cette page."
|
msgstr "Merci de vous identifier ou de créer un compte pour voir cette page."
|
||||||
|
|
||||||
#: core/templates/core/login.jinja
|
#: core/templates/core/login.jinja
|
||||||
msgid "Your username and password didn't match. Please try again."
|
msgid "Your credentials didn't match. Please try again."
|
||||||
msgstr ""
|
msgstr "Vos identifiants ne correspondent pas. Veuillez réessayer."
|
||||||
"Votre nom d'utilisateur et votre mot de passe ne correspondent pas. Merci de "
|
|
||||||
"réessayer."
|
|
||||||
|
|
||||||
#: core/templates/core/login.jinja
|
#: core/templates/core/login.jinja
|
||||||
msgid "Lost password?"
|
msgid "Lost password?"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user