July 4 - Update security
This commit is contained in:
+43
-9
@@ -5,9 +5,46 @@ from wtforms import (StringField, PasswordField, SelectField, TextAreaField,
|
||||
RadioField)
|
||||
from wtforms.validators import (DataRequired, Email, Length, EqualTo,
|
||||
Optional, NumberRange, ValidationError)
|
||||
import re as _re
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
# ── Password strength ──────────────────────────────────────────────────────────
|
||||
# A small blocklist of trivially weak passwords, matched case-insensitively.
|
||||
_COMMON_PASSWORDS = {
|
||||
'password', 'password1', 'password123', '12345678', '123456789',
|
||||
'qwerty123', 'qwertyui', '11111111', 'letmein1', 'welcome1', 'welcome123',
|
||||
'iloveyou', 'admin123', 'changeme1', 'passw0rd', 'abc12345', 'football1',
|
||||
}
|
||||
|
||||
|
||||
def strong_password(min_length=8):
|
||||
"""WTForms validator enforcing a baseline password strength.
|
||||
|
||||
Policy (NIST-aligned — length first, light complexity, blocklist):
|
||||
* at least ``min_length`` characters,
|
||||
* at least one letter AND one digit,
|
||||
* not a well-known weak password.
|
||||
|
||||
Skips empty values, so it can sit after ``Optional()`` on edit forms where a
|
||||
blank password means "leave the existing one unchanged".
|
||||
"""
|
||||
def _validator(form, field):
|
||||
pw = field.data or ''
|
||||
if not pw:
|
||||
return
|
||||
if len(pw) < min_length:
|
||||
raise ValidationError(
|
||||
f'Password must be at least {min_length} characters long.')
|
||||
if not (_re.search(r'[A-Za-z]', pw) and _re.search(r'\d', pw)):
|
||||
raise ValidationError(
|
||||
'Password must include at least one letter and one number.')
|
||||
if pw.lower() in _COMMON_PASSWORDS:
|
||||
raise ValidationError(
|
||||
'That password is too common — please choose a less predictable one.')
|
||||
return _validator
|
||||
|
||||
|
||||
# ── Auth ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
@@ -21,7 +58,7 @@ class ProfileForm(FlaskForm):
|
||||
full_name = StringField('Full Name', validators=[Optional(), Length(max=150)])
|
||||
email = StringField('Email', validators=[DataRequired(), Email(), Length(max=255)])
|
||||
current_password = PasswordField('Current Password', validators=[Optional()])
|
||||
new_password = PasswordField('New Password', validators=[Optional(), Length(min=6, max=100)])
|
||||
new_password = PasswordField('New Password', validators=[Optional(), Length(max=100), strong_password()])
|
||||
confirm_password = PasswordField('Confirm New Password', validators=[EqualTo('new_password', message='Passwords must match.')])
|
||||
|
||||
def __init__(self, user=None, *args, **kwargs):
|
||||
@@ -48,7 +85,7 @@ class UserForm(FlaskForm):
|
||||
username = StringField('Username', validators=[DataRequired(), Length(min=3, max=100)])
|
||||
full_name = StringField('Full Name', validators=[Optional(), Length(max=150)])
|
||||
email = StringField('Email', validators=[DataRequired(), Email(), Length(max=255)])
|
||||
password = PasswordField('Password', validators=[Optional(), Length(min=6, max=100)])
|
||||
password = PasswordField('Password', validators=[Optional(), Length(max=100), strong_password()])
|
||||
confirm_password = PasswordField('Confirm Password', validators=[Optional(), EqualTo('password')])
|
||||
role = SelectField('Role', choices=[
|
||||
('admin', 'Administrator'),
|
||||
@@ -67,10 +104,7 @@ class UserForm(FlaskForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.user = user
|
||||
|
||||
def validate_password(self, field):
|
||||
"""Enforce minimum length only when a new password is actually provided."""
|
||||
if field.data and len(field.data) < 6:
|
||||
raise ValidationError('Password must be at least 6 characters.')
|
||||
# Password strength is enforced by the strong_password() field validator.
|
||||
|
||||
def validate_confirm_password(self, field):
|
||||
"""Require confirmation to match only when a new password is provided."""
|
||||
@@ -212,7 +246,7 @@ class CustomerUserForm(FlaskForm):
|
||||
username = StringField('Username', validators=[DataRequired(), Length(min=3, max=100)])
|
||||
full_name = StringField('Full Name', validators=[Optional(), Length(max=150)])
|
||||
email = StringField('Email', validators=[DataRequired(), Email(), Length(max=255)])
|
||||
password = PasswordField('Password', validators=[Optional(), Length(min=8)])
|
||||
password = PasswordField('Password', validators=[Optional(), Length(max=128), strong_password()])
|
||||
confirm_password = PasswordField('Confirm Password',
|
||||
validators=[Optional(), EqualTo('password',
|
||||
message='Passwords must match.')])
|
||||
@@ -258,7 +292,7 @@ class ForgotPasswordForm(FlaskForm):
|
||||
|
||||
|
||||
class ResetPasswordForm(FlaskForm):
|
||||
password = PasswordField('New Password', validators=[DataRequired(), Length(min=8, max=100)])
|
||||
password = PasswordField('New Password', validators=[DataRequired(), Length(max=100), strong_password()])
|
||||
confirm_password = PasswordField('Confirm Password', validators=[DataRequired(),
|
||||
EqualTo('password', message='Passwords must match.')])
|
||||
|
||||
@@ -266,7 +300,7 @@ class ResetPasswordForm(FlaskForm):
|
||||
class SetPasswordForm(FlaskForm):
|
||||
"""Public form for customer to choose their username and password via emailed link."""
|
||||
username = StringField('Choose a Username', validators=[DataRequired(), Length(min=3, max=100)])
|
||||
password = PasswordField('Password', validators=[DataRequired(), Length(min=8, max=100)])
|
||||
password = PasswordField('Password', validators=[DataRequired(), Length(max=100), strong_password()])
|
||||
confirm_password = PasswordField('Confirm Password',
|
||||
validators=[DataRequired(),
|
||||
EqualTo('password', message='Passwords must match.')])
|
||||
|
||||
Reference in New Issue
Block a user