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
+73
View File
@@ -0,0 +1,73 @@
"""
tests/test_security_hardening.py
--------------------------------
Covers the security quick-wins:
* strong_password() validator — length, letter+digit, common-password blocklist
* response security headers — hardened CSP directives + HSTS over HTTPS only
"""
import pytest
from wtforms.validators import ValidationError
# ── Password strength validator ─────────────────────────────────────────────
class _Field:
def __init__(self, data):
self.data = data
def _accepts(pw, **kw):
from app.utils.forms import strong_password
try:
strong_password(**kw)(None, _Field(pw))
return True
except ValidationError:
return False
def test_strong_password_accepts_reasonable():
assert _accepts('abcd1234') # 8 chars, letter + digit
assert _accepts('Tr0ubador!!') # longer, mixed
assert _accepts('') # empty is skipped (Optional handles required-ness)
def test_strong_password_rejects_weak():
assert not _accepts('short1') # too short (< 8)
assert not _accepts('allletters') # no digit
assert not _accepts('12345678') # no letter
assert not _accepts('password1') # common-password blocklist
assert not _accepts('welcome1') # common-password blocklist
def test_strong_password_custom_min_length():
assert not _accepts('abcd123', min_length=8) # 7 chars
assert _accepts('abcd1234', min_length=8)
# ── Response security headers ────────────────────────────────────────────────
@pytest.fixture
def client(app):
return app.test_client()
def test_hardened_csp_present(client):
resp = client.get('/auth/login')
csp = resp.headers.get('Content-Security-Policy', '')
assert "object-src 'none'" in csp
assert "base-uri 'self'" in csp
assert "form-action 'self'" in csp
assert "frame-ancestors 'none'" in csp
assert resp.headers.get('X-Content-Type-Options') == 'nosniff'
def test_hsts_only_over_https(client):
# Plain HTTP request — no HSTS advertised.
http = client.get('/auth/login')
assert 'Strict-Transport-Security' not in http.headers
# Behind a TLS-terminating proxy (X-Forwarded-Proto=https) — HSTS present.
https = client.get('/auth/login', headers={'X-Forwarded-Proto': 'https'})
assert https.headers.get('Strict-Transport-Security', '').startswith('max-age=')