"""Fixes from the 2026-10-14 security assessment (ultimo-security-report.md):
cross-site requests refused (login CSRF), terse validation errors, the new-password
policy, link schemes in message HTML, and DNS-rebinding-proof outbound fetches."""
from __future__ import annotations
import asyncio
import uuid
from collections.abc import AsyncIterator
import httpx
import pytest
import pytest_asyncio
from httpx import ASGITransport
from starlette.datastructures import Headers
from app.accounts import ssrf
from app.config import get_settings
from app.identity import password_policy
from app.identity.password_policy import PasswordRejected, check_new_password
from app.indexer.sanitize import sanitize_html
from app.main import create_app
from app.security.cross_site import is_cross_site
@pytest_asyncio.fixture
async def client() -> AsyncIterator[httpx.AsyncClient]:
async with httpx.AsyncClient(
transport=ASGITransport(app=create_app()), base_url="http://mail.example.org"
) as c:
yield c
def _login(client: httpx.AsyncClient, headers: dict[str, str]): # type: ignore[no-untyped-def]
return client.post(
"/api/auth/login", json={"handle": "nobody", "password": "x"}, headers=headers
)
# --- cross-site requests (finding 3) --------------------------------------------------
@pytest.mark.parametrize(
"headers",
[
{"Sec-Fetch-Site": "cross-site", "Origin": "https://evil.example"},
{"Origin": "https://evil.example"}, # an older browser: Origin only
{"Origin": "null"}, # a sandboxed frame
],
)
async def test_sign_in_from_another_site_is_refused(
client: httpx.AsyncClient, headers: dict[str, str]
) -> None:
resp = await _login(client, headers)
assert resp.status_code == 403
assert resp.json()["error"]["code"] == "cross_site"
@pytest.mark.parametrize(
"headers",
[
{"Sec-Fetch-Site": "same-origin", "Origin": "http://mail.example.org"},
{"Origin": "http://mail.example.org"},
{"Origin": "https://mail.example.org:8443"}, # the port does not matter
{"Sec-Fetch-Site": "same-site"}, # our own subdomain
{"Sec-Fetch-Site": "none"}, # typed into the address bar
{}, # not a browser (scripts, mail apps)
],
)
async def test_sign_in_from_our_own_pages_goes_through(
client: httpx.AsyncClient, headers: dict[str, str]
) -> None:
resp = await _login(client, headers)
assert resp.status_code == 401 # evaluated: wrong credentials, not refused
assert resp.json()["error"]["code"] == "invalid_credentials"
async def test_reads_from_another_site_are_not_refused(client: httpx.AsyncClient) -> None:
resp = await client.get("/api/auth/registration", headers={"Sec-Fetch-Site": "cross-site"})
assert resp.status_code == 200
def test_origin_on_our_own_domain_passes_whatever_the_host_header() -> None:
domain = get_settings().app_domain
headers = Headers({"origin": f"https://{domain}", "host": "localhost:8088"})
assert is_cross_site(headers) is False
sub = Headers({"origin": f"https://mail.{domain}", "host": "localhost:8088"})
assert is_cross_site(sub) is False
lookalike = Headers({"origin": f"https://evil{domain}", "host": "localhost:8088"})
assert is_cross_site(lookalike) is True
def test_origin_matches_the_name_a_reverse_proxy_saw() -> None:
headers = Headers(
{"origin": "https://ultimo.email", "host": "web:80", "x-forwarded-host": "ultimo.email"}
)
assert is_cross_site(headers) is False
# --- validation errors (finding 5) ----------------------------------------------------
async def test_validation_errors_do_not_spell_out_the_request_model(
client: httpx.AsyncClient,
) -> None:
resp = await client.post("/api/auth/login", json={})
assert resp.status_code == 422
body = resp.json()
assert "detail" not in body
assert body["error"]["code"] == "validation_error"
assert body["error"]["message"].startswith("handle:")
async def test_a_too_short_password_says_so(client: httpx.AsyncClient) -> None:
resp = await client.post("/api/auth/register", json={"localpart": "abcde", "password": "123"})
assert resp.status_code == 422
assert "password" in resp.json()["error"]["message"]
async def test_new_passwords_are_capped_at_128(client: httpx.AsyncClient) -> None:
resp = await client.post(
"/api/auth/register", json={"localpart": "abcde", "password": "x" * 129}
)
assert resp.status_code == 422
# --- new-password policy (finding 6) --------------------------------------------------
@pytest.mark.parametrize(
"password",
["password123", "Password123", "12345678", "aaaaaaaaaa", "abababab", "jsmith2026"],
)
async def test_guessable_passwords_are_refused(password: str) -> None:
with pytest.raises(PasswordRejected):
await check_new_password(password, "jsmith@example.org")
async def test_a_decent_password_passes() -> None:
await check_new_password("correct horse battery", "jsmith@example.org")
async def test_breached_passwords_are_refused(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(get_settings(), "password_breach_check", True)
sha = "A" * 40
async def breached(_password: str) -> bool:
return True
monkeypatch.setattr(password_policy, "_breached", breached)
with pytest.raises(PasswordRejected) as err:
await check_new_password("long unusual phrase " + sha, "jsmith@example.org")
assert err.value.code == "breached_password"
async def test_the_breach_service_being_down_does_not_block_sign_up(
monkeypatch: pytest.MonkeyPatch,
) -> None:
def unreachable(request: httpx.Request) -> httpx.Response:
raise httpx.ConnectError("down", request=request)
real_client = httpx.AsyncClient
def client_factory(*args, **kwargs): # type: ignore[no-untyped-def]
return real_client(transport=httpx.MockTransport(unreachable))
monkeypatch.setattr(password_policy.httpx, "AsyncClient", client_factory)
assert await password_policy._breached("anything at all") is False
async def test_sign_up_with_a_common_password_is_refused(client: httpx.AsyncClient) -> None:
resp = await client.post(
"/api/auth/register",
json={"localpart": f"pw{uuid.uuid4().hex[:8]}", "password": "password123"},
)
assert resp.status_code == 400
assert resp.json()["error"]["code"] == "weak_password"
# --- message HTML (V2) ----------------------------------------------------------------
@pytest.mark.parametrize(
"html",
[
'x',
'x',
'x',
'x',
"
",
"",
"