July 4 - Update security

This commit is contained in:
2026-07-04 15:49:47 -04:00
parent 1cfb387b81
commit 03ce083691
6 changed files with 137 additions and 11 deletions
+43 -9
View File
@@ -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.')])