gabriel / musehub public
validation.py python
55 lines 2.0 KB
Raw
sha256:7d6dd8f4a89e2d1fef2d84f6e65feaff51385d382f466766b7f690a22ec18e32 fix: fall back to DB ancestry check when mpack-only fast-fo… Sonnet 4.6 patch 7 days ago
1 """Validated path-segment types for FastAPI route parameters.
2
3 All URL path segments that come from user input are validated against an
4 allowlist regex and capped at a safe length. FastAPI enforces these at
5 decode time and returns 422 Unprocessable Entity on violation — before any
6 handler logic runs.
7
8 Usage in route signatures::
9
10 from musehub.api.validation import SlugParam, BranchParam, FilePathParam
11
12 async def my_route(owner: SlugParam, repo_slug: SlugParam, branch: BranchParam):
13 ...
14 """
15
16 from typing import Annotated
17
18 from fastapi import Path
19
20 # owner / slug / repo_slug — alphanumeric, underscore, hyphen, dot.
21 # Must start with alphanumeric. Max 100 characters.
22 # Blocks: slashes, null bytes, "..", shell metacharacters, Unicode overlong sequences.
23 _SLUG_RE = r"^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,99}$"
24
25 # branch — same charset as slug but allows "/" for branch namespaces (e.g. feat/foo).
26 # Pydantic uses Rust's regex engine which does not support lookaheads, so we
27 # block ".." structurally: each path component must start with [a-zA-Z0-9_],
28 # meaning no component can be "." or "..".
29 # Pattern: one or more components of the form [a-zA-Z0-9_][a-zA-Z0-9_.-]*,
30 # optionally separated by "/". Max 200 characters.
31 _BRANCH_RE = r"^[a-zA-Z0-9_][a-zA-Z0-9_.-]*(/[a-zA-Z0-9_][a-zA-Z0-9_.-]*)*$"
32
33 # file path inside a repo (e.g. "src/main.py", "README.md").
34 # Each path component is either:
35 # - a normal name: starts with [a-zA-Z0-9_]
36 # - a dotfile: starts with "." followed immediately by [a-zA-Z0-9_]
37 # This structurally blocks ".", "..", and traversal sequences without lookaheads.
38 # Max 1000 characters enforced by max_length on the Path() field info.
39 _SEGMENT = r"(?:\.[a-zA-Z0-9_]|[a-zA-Z0-9_])[a-zA-Z0-9_.-]*"
40 _FILE_PATH_RE = rf"^{_SEGMENT}(\/{_SEGMENT})*$"
41
42 SlugParam = Annotated[
43 str,
44 Path(pattern=_SLUG_RE, max_length=100, min_length=1),
45 ]
46
47 BranchParam = Annotated[
48 str,
49 Path(pattern=_BRANCH_RE, max_length=200, min_length=1),
50 ]
51
52 FilePathParam = Annotated[
53 str,
54 Path(pattern=_FILE_PATH_RE, max_length=1000, min_length=1),
55 ]
File History 1 commit
sha256:7d6dd8f4a89e2d1fef2d84f6e65feaff51385d382f466766b7f690a22ec18e32 fix: fall back to DB ancestry check when mpack-only fast-fo… Sonnet 4.6 patch 7 days ago