Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion apps/accounts/serializers.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
from allauth.account.models import EmailAddress
from base.utils import get_user_by_email
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User
from participants.utils import (
has_participated_in_require_complete_profile_challenge,
)
from rest_auth.serializers import PasswordResetSerializer
from rest_auth.registration.serializers import RegisterSerializer
from rest_auth.serializers import LoginSerializer, PasswordResetSerializer
from rest_framework import serializers
from rest_framework.exceptions import ValidationError

from .models import JwtToken, Profile

PASSWORD_MAX_LENGTH = getattr(settings, "PASSWORD_MAX_LENGTH", 128)

LOCKED_PROFILE_FIELDS = [
"first_name",
"last_name",
Expand Down Expand Up @@ -337,6 +341,26 @@ def create(self, validated_data):
return email_address


class CustomRegisterSerializer(RegisterSerializer):
"""Registration serializer with bounded password field lengths."""

password1 = serializers.CharField(
write_only=True, max_length=PASSWORD_MAX_LENGTH
)
password2 = serializers.CharField(
write_only=True, max_length=PASSWORD_MAX_LENGTH
)


class CustomLoginSerializer(LoginSerializer):
"""Login serializer with bounded password field length."""

password = serializers.CharField(
style={"input_type": "password"},
max_length=PASSWORD_MAX_LENGTH,
)


class CustomPasswordResetSerializer(PasswordResetSerializer):
"""
Send password reset emails only for eligible accounts while always
Expand Down
14 changes: 14 additions & 0 deletions apps/accounts/throttles.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ def get_cache_key(self, request, view):
return self.cache_format % {"scope": self.scope, "ident": email}


class RegistrationIPThrottle(_ThrottlingCacheMixin, SimpleRateThrottle):
"""
Limit registration requests to 10/hour per client IP address.
"""

scope = "registration_ip"

def get_cache_key(self, request, view):
return self.cache_format % {
"scope": self.scope,
"ident": self.get_ident(request),
}


class PasswordResetIPThrottle(_ThrottlingCacheMixin, SimpleRateThrottle):
"""
Limit password reset requests to 10/hour per client IP address.
Expand Down
32 changes: 32 additions & 0 deletions apps/accounts/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _


class MaximumLengthValidator:
"""
Reject passwords longer than PASSWORD_MAX_LENGTH to prevent resource
exhaustion from oversized password payloads.
"""

def __init__(self, max_length=None):
self.max_length = max_length or getattr(
settings, "PASSWORD_MAX_LENGTH", 128
)

def validate(self, password, user=None):
if len(password) > self.max_length:
raise ValidationError(
_(
"This password is longer than the maximum of "
"%(max_length)d characters."
),
code="password_too_long",
params={"max_length": self.max_length},
)

def get_help_text(self):
return _(
"Your password must be at most %(max_length)d characters."
% {"max_length": self.max_length}
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
3 changes: 3 additions & 0 deletions apps/accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from .throttles import (
PasswordResetEmailThrottle,
PasswordResetIPThrottle,
RegistrationIPThrottle,
ResendEmailThrottle,
)

Expand Down Expand Up @@ -65,6 +66,8 @@ class SafeRegisterView(RegisterView):
slip past the check. This view acts as a safety net for that case.
"""

throttle_classes = (RegistrationIPThrottle,)

def create(self, request, *args, **kwargs):
try:
return super().create(request, *args, **kwargs)
Expand Down
6 changes: 5 additions & 1 deletion apps/challenges/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
DEFAULT_WORKER_PYTHON_VERSION,
SUPPORTED_WORKER_PYTHON_VERSIONS,
)
from django.conf import settings
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError as DjangoValidationError
from hosts.serializers import ChallengeHostTeamSerializer
Expand Down Expand Up @@ -615,7 +616,10 @@ class ChallengeInvitationRegisterSerializer(serializers.Serializer):
last_name = serializers.CharField(
max_length=150, required=False, allow_blank=True
)
password = serializers.CharField(write_only=True)
password = serializers.CharField(
write_only=True,
max_length=getattr(settings, "PASSWORD_MAX_LENGTH", 128),
)

def validate(self, attrs):
user = self.context.get("user")
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/views/web/auth/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
</div>
<!-- password -->
<div class="input-field align-left">
<input type="{{canShowPassword ? 'text' : 'password'}}" id="password" class="dark-autofill" name="password" ng-model="auth.getUser.password" autocomplete="new-password" ng-minlength="8" required>
<input type="{{canShowPassword ? 'text' : 'password'}}" id="password" class="dark-autofill" name="password" ng-model="auth.getUser.password" autocomplete="new-password" ng-minlength="8" ng-maxlength="128" required>
<span class="form-icon form-icon-dark" ng-click="auth.togglePasswordVisibility()">
<i ng-if="!canShowPassword" class="fa fa-eye pointer"></i>
<i ng-if="canShowPassword" class="fa fa-eye-slash pointer"></i>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/views/web/auth/reset-password-confirm.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<form name="resetconfirmForm" ng-submit="auth.resetPasswordConfirm(resetconfirmForm.$valid)" class="reset-password" novalidate>
<!-- email -->
<div class="input-field align-left">
<input type="{{canShowPassword ? 'text' : 'password'}}" id="new_password1" onpaste="return false;" class="dark-autofill" name="new_password1" ng-model="auth.getUser.new_password1" minlength="8" required>
<input type="{{canShowPassword ? 'text' : 'password'}}" id="new_password1" onpaste="return false;" class="dark-autofill" name="new_password1" ng-model="auth.getUser.new_password1" minlength="8" maxlength="128" required>
<span class="form-icon form-icon-dark" ng-click="auth.togglePasswordVisibility()">
<i ng-if="!canShowPassword" class="fa fa-eye pointer"></i>
<i ng-if="canShowPassword" class="fa fa-eye-slash pointer"></i>
Expand All @@ -31,7 +31,7 @@
</div>
</div>
<div class="input-field align-left">
<input type="{{canShowConfirmPassword ? 'text' : 'password'}}" id="new_password2" class="dark-autofill" name="new_password2" ng-model="auth.getUser.new_password2" minlength="8" compare-to="auth.getUser.new_password1" required>
<input type="{{canShowConfirmPassword ? 'text' : 'password'}}" id="new_password2" class="dark-autofill" name="new_password2" ng-model="auth.getUser.new_password2" minlength="8" maxlength="128" compare-to="auth.getUser.new_password1" required>
<span class="form-icon form-icon-dark" ng-click="auth.toggleConfirmPasswordVisibility()">
<i ng-if="!canShowConfirmPassword" class="fa fa-eye pointer"></i>
<i ng-if="canShowConfirmPassword" class="fa fa-eye-slash pointer"></i>
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/views/web/auth/signup.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
</div>
<!-- password -->
<div class="input-field align-left">
<input type="{{canShowPassword ? 'text' : 'password'}}" id="password" class="dark-autofill" name="password" ng-model="auth.regUser.password" ng-minlength="8" ng-change="auth.checkStrength(auth.regUser.password)" required>
<input type="{{canShowPassword ? 'text' : 'password'}}" id="password" class="dark-autofill" name="password" ng-model="auth.regUser.password" ng-minlength="8" ng-maxlength="128" ng-change="auth.checkStrength(auth.regUser.password)" required>
<span class="form-icon form-icon-dark" ng-click="auth.togglePasswordVisibility()">
<i ng-if="!canShowPassword" class="fa fa-eye pointer"></i>
<i ng-if="canShowPassword" class="fa fa-eye-slash pointer"></i>
Expand All @@ -41,12 +41,13 @@
</label>
<div class="wrn-msg text-highlight" ng-messages="signupForm.password.$error" ng-if="signupForm.password.$touched || signupForm.$submitted">
<p ng-message="minlength">Password is less than 8 characters.</p>
<p ng-message="maxlength">Password must be at most 128 characters.</p>
<p ng-message="required">Password is required.</p>
</div>
</div>
<!-- confirm password -->
<div class="input-field align-left">
<input type="{{canShowConfirmPassword ? 'text' : 'password'}}" id="confirm_password" class="dark-autofill" name="confirm_password" ng-model="auth.regUser.confirm_password" ng-minlength="8" compare-to="auth.regUser.password" required>
<input type="{{canShowConfirmPassword ? 'text' : 'password'}}" id="confirm_password" class="dark-autofill" name="confirm_password" ng-model="auth.regUser.confirm_password" ng-minlength="8" ng-maxlength="128" compare-to="auth.regUser.password" required>
<span class="form-icon form-icon-dark" ng-click="auth.toggleConfirmPasswordVisibility()">
<i ng-if="!canShowConfirmPassword" class="fa fa-eye pointer"></i>
<i ng-if="canShowConfirmPassword" class="fa fa-eye-slash pointer"></i>
Expand All @@ -55,6 +56,7 @@
<div class="wrn-msg text-highlight" ng-messages="signupForm.confirm_password.$error" ng-if="signupForm.confirm_password.$touched || signupForm.$submitted">
<p ng-message="compareTo">Passwords do not match</p>
<p ng-message="minlength">Password is less than 8 characters.</p>
<p ng-message="maxlength">Password must be at most 128 characters.</p>
<p ng-message="required">Password confirmation is required.</p>
</div>
</div>
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/views/web/challenge-invite.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
<div class="col s12 m12">
<div class="input-field align-left">
<input type="password" id="password" onpaste="return false;" class="dark-autofill" name="password"
ng-model="challenge_invitation.password" ng-minlength="8" required>
ng-model="challenge_invitation.password" ng-minlength="8" ng-maxlength="128" required>
<span class="form-icon form-icon-dark"><i class="fa fa-key"></i></span>
<label for="password">
<strong>Password-minlength:8 required*</strong>
Expand All @@ -79,6 +79,7 @@
<div class="wrn-msg text-highlight" ng-messages="registerChallengeParticipantForm.password.$error"
ng-if="registerChallengeParticipantForm.password.$touched || registerChallengeParticipantForm.$submitted">
<p ng-message="minlength">Password is less than 8 characters.</p>
<p ng-message="maxlength">Password must be at most 128 characters.</p>
<p ng-message="required">Password is required.</p>
</div>
</div>
Expand All @@ -88,13 +89,14 @@
<div class="col s12 m12">
<div class="input-field align-left">
<input type="password" id="confirm_password" onpaste="return false;" class="dark-autofill" name="confirm_password"
ng-model="challenge_invitation.confirm_password" ng-minlength="8" compare-to="challenge_invitation.password" required>
ng-model="challenge_invitation.confirm_password" ng-minlength="8" ng-maxlength="128" compare-to="challenge_invitation.password" required>
<span class="form-icon form-icon-dark"><i class="fa fa-check-circle"></i></span>
<label for="confirm_password">Confirm Password *</label>
<div class="wrn-msg text-highlight" ng-messages="registerChallengeParticipantForm.confirm_password.$error"
ng-if="registerChallengeParticipantForm.confirm_password.$touched || registerChallengeParticipantForm.$submitted">
<p ng-message="compareTo">Passwords do not match</p>
<p ng-message="minlength">Password is less than 8 characters.</p>
<p ng-message="maxlength">Password must be at most 128 characters.</p>
<p ng-message="required">Password confirmation is required.</p>
</div>
</div>
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/views/web/change-password.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<div class="pass-title">Change Password</div>
<form name="profileForm" ng-submit="profile.changePassword(profileForm.$valid)" novalidate>
<div class="input-field align-left">
<input id="old_password" name="old_password" type="{{canShowOldPassword ? 'text' : 'password'}}" class="text-dark-black dark-autofill w-400" ng-model="profile.user.old_password" ng-minlength="8" required focus-if>
<input id="old_password" name="old_password" type="{{canShowOldPassword ? 'text' : 'password'}}" class="text-dark-black dark-autofill w-400" ng-model="profile.user.old_password" ng-minlength="8" ng-maxlength="128" required focus-if>
<span class="form-icon form-icon-dark" ng-click="profile.toggleOldPasswordVisibility()">
<i ng-if="!canShowOldPassword" class="fa fa-eye pointer"></i>
<i ng-if="canShowOldPassword" class="fa fa-eye-slash pointer"></i>
Expand All @@ -19,7 +19,7 @@
</div>
</div>
<div class="input-field align-left">
<input id="new_password1" name="new_password1" type="{{canShowNewPassword ? 'text' : 'password'}}" class="text-dark-black dark-autofill w-400" ng-model="profile.user.new_password1" ng-minlength="8" required match="profile.user.old_password">
<input id="new_password1" name="new_password1" type="{{canShowNewPassword ? 'text' : 'password'}}" class="text-dark-black dark-autofill w-400" ng-model="profile.user.new_password1" ng-minlength="8" ng-maxlength="128" required match="profile.user.old_password">
<span class="form-icon form-icon-dark" ng-click="profile.toggleNewPasswordVisibility()">
<i ng-if="!canShowNewPassword" class="fa fa-eye pointer"></i>
<i ng-if="canShowNewPassword" class="fa fa-eye-slash pointer"></i>
Expand All @@ -32,7 +32,7 @@
</div>
</div>
<div class="input-field align-left">
<input id="new_password2" name="new_password2" type="{{canShowNewConfirmPassword ? 'text' : 'password'}}" class="text-dark-black dark-autofill w-400" ng-model="profile.user.new_password2" ng-minlength="8" compare-to="profile.user.new_password1" required>
<input id="new_password2" name="new_password2" type="{{canShowNewConfirmPassword ? 'text' : 'password'}}" class="text-dark-black dark-autofill w-400" ng-model="profile.user.new_password2" ng-minlength="8" ng-maxlength="128" compare-to="profile.user.new_password1" required>
<span class="form-icon form-icon-dark" ng-click="profile.toggleNewConfirmVisibility()">
<i ng-if="!canShowNewConfirmPassword" class="fa fa-eye pointer"></i>
<i ng-if="canShowNewConfirmPassword" class="fa fa-eye-slash pointer"></i>
Expand Down
14 changes: 13 additions & 1 deletion settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,19 @@
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators

PASSWORD_MAX_LENGTH = 128

AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" # noqa
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator" # noqa
},
{
"NAME": "accounts.validators.MaximumLengthValidator",
"OPTIONS": {"max_length": PASSWORD_MAX_LENGTH},
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator" # noqa
},
Expand Down Expand Up @@ -185,6 +191,7 @@
"resend_email": "3/hour",
"password_reset_email": "3/hour",
"password_reset_ip": "10/hour",
"registration_ip": "10/hour",
},
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
Expand Down Expand Up @@ -332,7 +339,7 @@
# The maximum size in bytes for request body
# https://docs.djangoproject.com/en/1.10/ref/settings/#data-upload-max-memory-size
FILE_UPLOAD_MAX_MEMORY_SIZE = 4294967296 # 4 GB
DATA_UPLOAD_MAX_MEMORY_SIZE = 4294967296 # 4 GB
DATA_UPLOAD_MAX_MEMORY_SIZE = 10485760 # 10 MB
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

# Maximum number of GET/POST parameters for forms
# https://docs.djangoproject.com/en/1.10/ref/settings/#data-upload-max-number-fields
Expand All @@ -344,6 +351,11 @@
REST_AUTH_SERIALIZERS = {
"USER_DETAILS_SERIALIZER": "accounts.serializers.ProfileSerializer",
"PASSWORD_RESET_SERIALIZER": "accounts.serializers.CustomPasswordResetSerializer",
"LOGIN_SERIALIZER": "accounts.serializers.CustomLoginSerializer",
}

REST_AUTH_REGISTER_SERIALIZERS = {
"REGISTER_SERIALIZER": "accounts.serializers.CustomRegisterSerializer",
}

# Default email for sending emails..
Expand Down
21 changes: 21 additions & 0 deletions tests/unit/accounts/test_validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.conf import settings
from django.core.exceptions import ValidationError
from django.test import SimpleTestCase

from accounts.validators import MaximumLengthValidator


class MaximumLengthValidatorTest(SimpleTestCase):
def test_accepts_password_within_limit(self):
validator = MaximumLengthValidator(
max_length=settings.PASSWORD_MAX_LENGTH
)
validator.validate("a" * settings.PASSWORD_MAX_LENGTH)

def test_rejects_password_over_limit(self):
validator = MaximumLengthValidator(
max_length=settings.PASSWORD_MAX_LENGTH
)
with self.assertRaises(ValidationError) as context:
validator.validate("a" * (settings.PASSWORD_MAX_LENGTH + 1))
self.assertEqual(context.exception.code, "password_too_long")
Loading
Loading