July 4 - Update security
This commit is contained in:
@@ -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=')
|
||||
Reference in New Issue
Block a user