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

1"""UUID shape validation for parameterized page-route ids. 

2 

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. 

10 

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""" 

27 

28import uuid 

29 

30from fastapi import HTTPException 

31from loguru import logger 

32 

33 

34def validated_uuid_path_param(value: object, label: str) -> str: 

35 """Return *value*'s canonical UUID form if well-formed, else 404. 

36 

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. 

53 

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