diff --git a/Docs/Security.md b/Docs/Security.md index 74b1f24..d887f26 100644 --- a/Docs/Security.md +++ b/Docs/Security.md @@ -88,6 +88,7 @@ See `backend/app/middleware/csrf.py` and `backend/app/middleware/rate_limit.py` - Passwords are hashed with SHA256 on the frontend before transmission - The backend never stores plain-text passwords - See `backend/app/services/auth.py` for authentication implementation +- **Common password prevention:** The setup validator rejects a list of ~75 common plaintext passwords that pass structural complexity checks (e.g., `Password1!`). The list is embedded in `backend/app/models/setup.py` and is checked case-insensitively. --- diff --git a/Docs/Tasks.md b/Docs/Tasks.md index 86384ff..1fd4b4a 100644 --- a/Docs/Tasks.md +++ b/Docs/Tasks.md @@ -1,39 +1,3 @@ -### Issue #30: LOW-MEDIUM - IPv4-Mapped IPv6 Address Duplicates - -**Where found**: -- `backend/app/utils/ip_utils.py` -- Treats "192.168.1.1" and "::ffff:192.168.1.1" as different IPs - -**Why this is needed**: -Same IP can be banned twice in different formats, causing: -- Duplicate ban logs -- Geo cache duplicates -- Analytics skewed - -**Goal**: -Normalize IP addresses to canonical form. - -**What to do**: -1. Add normalization: - ```python - def normalize_ip(ip_str: str) -> str: - ip = ipaddress.ip_address(ip_str) - # Convert IPv4-mapped IPv6 to IPv4 - if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped: - return str(ip.ipv4_mapped) - return str(ip) - ``` -2. Apply on all IP inputs (ban, import, etc.) -3. Test with various formats - -**Docs changes needed**: -- Document IP normalization - -**Doc references**: -- DETAILED_FINDINGS.md - Issue #22 "IPv4-Mapped IPv6" - ---- - ### Issue #31: LOW-MEDIUM - Weak Master Password Validation **Where found**: diff --git a/backend/tests/test_models.py b/backend/tests/test_models.py index 42bfe6b..43567e9 100644 --- a/backend/tests/test_models.py +++ b/backend/tests/test_models.py @@ -191,6 +191,35 @@ def test_setup_request_master_password_complexity_still_enforced() -> None: assert "special character" in str(exc_info.value) +def test_setup_request_rejects_common_passwords() -> None: + """SetupRequest rejects common passwords that pass all other complexity checks.""" + from app.models.setup import SetupRequest + + # Passw0rd! passes length (10), uppercase, digit, special checks but is too common + with pytest.raises(ValidationError) as exc_info: + SetupRequest(master_password="Passw0rd!") + assert "too common" in str(exc_info.value).lower() + + +def test_setup_request_accepts_valid_unique_password() -> None: + """SetupRequest accepts a password that meets all requirements and is not common.""" + from app.models.setup import SetupRequest + + req = SetupRequest(master_password="MyV3ryStr0ng!P@ssw0rd") + assert req.master_password == "MyV3ryStr0ng!P@ssw0rd" + + +def test_setup_request_rejects_short_common_passwords() -> None: + """SetupRequest rejects short common passwords (rejected for length, not common check).""" + from app.models.setup import SetupRequest + + for password in ["letmein", "admin", "qwerty", "shadow"]: + with pytest.raises(ValidationError) as exc_info: + SetupRequest(master_password=password) + # These fail the minimum length check first + assert "at least 8 characters" in str(exc_info.value) + + # --------------------------------------------------------------------------- # DashboardBanItem country_code validator # ---------------------------------------------------------------------------