Coverage for src/local_deep_research/web/utils/path_ids.py: 100%
11 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 21:20 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 21:20 +0000
1"""UUID shape validation for parameterized page-route ids.
3Page routes that interpolate a path parameter into templates validate
4the id's *shape* at the boundary: collection/note/document ids are
5server-generated UUID4 strings (``String(36)`` columns assigned from
6``str(uuid.uuid4())``), so anything else is a typo or an injection
7attempt. Shape validation is strictly more robust than fixing any one
8template's escaping — it holds for every interpolation context,
9including inline-JS handlers where HTML-escaping is not sufficient.
11Same pattern as ``_validated_collection_id`` (the stored-reflection XSS
12fix for the collection pages), but that helper is not reused here: it
13stays a local twin in ``rag.py`` so the route-table parity scanner
14(which only follows same-module dependency closures) still sees its
15404 for the collection routes. This shared helper is for page-route
16fences outside ``rag.py`` instead. Not every page route is fenced yet:
17``GET /chat/{session_id}`` and ``GET
18/library/document/{document_id}/txt`` still pass their raw path
19parameter (``session_id``, ``document_id``) into template contexts.
20``GET /library/document/{document_id}`` is unfenced too, but does not
21pass the raw id into its template — it looks the document up first and
22passes the object instead (``context={"request": request, "document":
23document}``, library.py). No known live vulnerability at the two
24raw-id sites (autoescaped attribute contexts) — they are the same
25hardening tier and remain to be fenced.
26"""
28import uuid
30from fastapi import HTTPException
31from loguru import logger
34def validated_uuid_path_param(value: object, label: str) -> str:
35 """Return *value*'s canonical UUID form if well-formed, else 404.
37 Returns ``str(uuid.UUID(str(value)))`` rather than echoing *value*
38 back verbatim, so alias spellings of the same UUID — for example an
39 ``urn:uuid:`` prefix, ``{...}`` braces, hyphen-free hex or uppercase
40 hex; ``uuid.UUID`` tolerates a few more, such as ``_`` digit
41 separators — come back canonical, which is what makes "well-formed
42 UUID string" above actually true of the return value. Length is the
43 binding reject: a padded input that pushes the total past 32
44 characters fails the parser and 404s; shorter padded inputs
45 (e.g. leading/trailing whitespace) are accepted by ``int(..., 16)``
46 and canonicalise with zero-padding, and CPython's base-16 parser
47 also accepts Unicode digit characters (`"𝟏"` / Arabic-Indic
48 digits), so this validator is NOT a strict whitelist of ASCII
49 hex — the canonical output is pure ASCII hex, which is what makes
50 it safe to interpolate in any template context. Ids as stored are
51 already ``str(uuid.uuid4())`` (lowercase, hyphenated, no wrapper),
52 so canonicalising a real id is a no-op round-trip.
54 A malformed id cannot match a real row, so 404 is the honest
55 answer for both typos and injection attempts.
56 """
57 try:
58 canonical = str(uuid.UUID(str(value)))
59 except (ValueError, AttributeError, TypeError):
60 value_preview = repr(value)[:100]
61 logger.warning("Rejected malformed {}: {}", label, value_preview)
62 raise HTTPException(
63 status_code=404, detail=f"{label} not found"
64 ) from None
65 return canonical