Skip to content

Commit 99d8a00

Browse files
fsecada01claude
andauthored
docs(security): CSRF/CSWSH coverage audit + per-adapter guidance (A4, #21) (#36)
Claude-Session: https://claude.ai/code/session_01JpsdVdokfe732D9aQKTXXQ Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 74a37a7 commit 99d8a00

3 files changed

Lines changed: 74 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ Optimize:
252252

253253
## Security Considerations
254254

255-
- CSRF protection required for mutations
255+
- CSRF: enforced automatically only on the Django adapter (via CsrfViewMiddleware); FastAPI/Litestar/Flask require host-level integration — see docs/SECURITY_CSRF.md
256256
- Input validation (Pydantic schemas)
257257
- State validation (don't trust client)
258258
- Permission checks (use Django permissions/FastAPI dependencies)

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ Generated from docstrings by pdoc and deployed to GitHub Pages on every push to
240240
| [Class-Based Views](docs/CBV_GUIDE.md) | CBV auth/permission patterns |
241241
| [E-Commerce Example](docs/examples/ecommerce.md) | Real-time cart + product demo |
242242
| [Multi-Step Wizard](docs/examples/wizard.md) | FastAPI wizard recipe (unsigned state — pending Epic A1) |
243+
| [CSRF & CSWSH Guide](docs/SECURITY_CSRF.md) | Per-adapter CSRF coverage audit + WebSocket hijacking guidance |
243244

244245
### AI / LLM Context
245246

docs/SECURITY_CSRF.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# CSRF & Cross-Site WebSocket Hijacking — Coverage & Integration Guide
2+
3+
**Scope:** HTTP mutation surfaces (`POST /components/{name}`, `.../stream`) and WebSocket handlers across the FastAPI, Litestar, Flask, and Django adapters.
4+
**Audit date:** 2026-07-01 · **Branch state:** audited read-only against `master` (Issue #21 / A4).
5+
**Verdict in one line:** CSRF is enforced **only** on the Django adapter, and only when the host app keeps `CsrfViewMiddleware` enabled and avoids the shipped `csrf_exempt` variants. FastAPI, Litestar, and Flask adapters have **zero** CSRF enforcement. All WebSocket handlers accept connections with no origin check or handshake token (CSWSH-exposed).
6+
7+
---
8+
9+
## 1. Threat model & exploitability framing (read this first)
10+
11+
CSRF only matters when the endpoint authenticates the caller via **ambient credentials** — a session cookie the browser attaches automatically. The component endpoints are **unauthenticated by default** (`permission_classes` defaults to `[AllowAny]`, and the non-Django adapters have no auth hook at all). Two consequences:
12+
13+
- **Default deployment (no auth):** the endpoints are already world-writable; there is no privileged session to ride, so "CSRF" is academic — the real exposure is the missing auth, not the missing token.
14+
- **The dangerous configuration** is the *natural next step* an integrator takes: bolt cookie/session auth onto these routes (Django login, `AuthenticatedComponentView`, a FastAPI/Litestar/Flask session cookie) **without** adding CSRF. At that point every mutation endpoint is CSRF-exploitable on the three non-Django adapters.
15+
16+
**Aggravating factor — form-encoded bodies are accepted.** Every HTTP adapter's `_parse_request_data` falls back to `application/x-www-form-urlencoded` when the content-type isn't JSON (`fastapi.py:48-52`, `litestar.py:48-52`, `flask.py:81-86`, `django_views.py:57-63`). That means an attacker does **not** need `fetch`/CORS/preflight — a plain auto-submitting cross-origin `<form>` reaches the handler and mutates state. This is the classic, no-JS-required CSRF vector, and it is wide open on FastAPI/Litestar/Flask the moment cookie auth exists.
17+
18+
**The client is not the mitigation.** `component-client.js` reads a Django CSRF token (`readCsrfToken()`, lines 54-64) and sends `X-CSRFToken` (lines 189-193). That header is *only* meaningful to Django; nothing on the FastAPI/Litestar/Flask side reads or validates it. It is also trivially bypassed — an attacker writes their own request and simply omits the header.
19+
20+
---
21+
22+
## 2. Per-adapter coverage table
23+
24+
| Adapter | CSRF enforcement today | Gap | Recommended integration | Exploitable in default deploy? |
25+
|---|---|---|---|---|
26+
| **FastAPI** (`adapters/fastapi.py`) | **None.** Routes added via `add_api_route(..., methods=["POST"])` (lines 170-181). No dependency, no token check. FastAPI ships no built-in CSRF. | Full CSRF gap. Accepts form-encoded bodies → plain cross-origin form works. | Document + optionally wire `fastapi-csrf-protect` (double-submit cookie) as an opt-in `Depends`; or instruct integrators to enforce CSRF at a middleware/proxy layer. Provide a hook so the token is verified before `async_dispatch`. | Only if the app adds cookie/session auth. No auth by default → CSRF moot but endpoint is unauthenticated-open. |
27+
| **Litestar** (`adapters/litestar.py`) | **None.** `@post` handlers (lines 79, 119). No `csrf_config` referenced or required. | Full CSRF gap. Litestar has a **built-in `CSRFConfig`** that the adapter neither wires nor documents. | Document that integrators pass `csrf_config=CSRFConfig(secret=...)` to the `Litestar(...)` app; verify our handlers honor the `_csrf_token` field / `x-csrftoken` header Litestar expects. Lowest-effort real fix of the three — the framework already has the machinery. | Same as FastAPI: only under cookie/session auth. |
28+
| **Flask** (`adapters/flask.py`) | **None.** Blueprint `POST /<name>` (lines 174-183). Flask ships no built-in CSRF. | Full CSRF gap. `strict_slashes=False` widens the match surface but is not itself the risk. | Document Flask-WTF `CSRFProtect` or Flask-SeaSurf; provide guidance to `csrf.protect()` the blueprint or exempt-and-re-protect. Consider an opt-in decorator in the adapter. | Same: only under cookie/session auth. Flask's newest adapter, least battle-tested. |
29+
| **Django (FBV `component_view`)** (`adapters/django_views.py:25-116`) | **Yes, by inheritance.** Not `csrf_exempt`; Django's `CsrfViewMiddleware` enforces the token when the middleware is in `MIDDLEWARE`. Header name `X-CSRFToken` matches Django's default `CSRF_HEADER_NAME`, and `component-client.js` sends exactly that. | Coverage is **conditional**, not intrinsic: breaks if the host removes the middleware, and the package *ships* bypasses (`component_view_no_csrf` line 116; `CSRFExemptComponentView` line 361). No `ensure_csrf_cookie` is provided by the framework — the host page must set the cookie, else the JS finds no token. | Keep the non-exempt view as the documented default. Doc note: enable `CsrfViewMiddleware`, ensure the CSRF cookie is set on the host page, avoid the exempt variants unless an alternative protection exists. | Not exploitable in a standard Django deploy (middleware on). Exploitable only via `component_view_no_csrf` / `CSRFExemptComponentView`, or disabled middleware (as `examples/django_example` does "for demo"). |
30+
| **Django (CBV `ComponentView` + subclasses)** | **Yes, same basis** as FBV (not exempt). `AuthenticatedComponentView` (line 312) adds cookie/session auth — so here CSRF actually *matters* and Django's middleware covers it. | `CSRFExemptComponentView` (line 361) is an explicit, documented opt-out that removes protection from an authenticated surface. | Same as FBV. Strengthen the docstring/doc warning on the exempt view: only for APIs using token/bearer auth, never for cookie-session flows. | Not by default; exploitable only via the exempt subclass. |
31+
32+
---
33+
34+
## 3. WebSocket CSRF / CSWSH limitation
35+
36+
All four WS paths accept the socket **immediately and unconditionally**:
37+
38+
- `core/websocket.py` — transport-agnostic `ComponentWebSocketManager.handle_message` dispatches `component_event` with no auth/origin concept (lines 77-154).
39+
- `adapters/fastapi_websocket.py:52``await websocket.accept()` then loop. No origin check, no token at handshake.
40+
- `adapters/litestar_websocket.py:55` — same pattern.
41+
- `adapters/django_websocket.py:62``await self.accept()` in `ComponentConsumer.connect()`. Django's `CsrfViewMiddleware` does **not** run on Channels.
42+
43+
**Limitation to document:** there is no origin validation and no handshake token on any adapter. Browsers do **not** enforce same-origin on WebSocket handshakes, and they **do** attach cookies to cross-origin WS requests. Any WS handler that trusts an ambient session is exposed to **Cross-Site WebSocket Hijacking (CSWSH)** — a malicious page can open a socket as the victim and drive `component_event` mutations.
44+
45+
**Guidance:**
46+
- **Django Channels:** wrap the consumer in `AllowedHostsOriginValidator` (or `OriginValidator`) in `asgi.py`/routing. Document as required for authenticated WS.
47+
- **FastAPI / Litestar:** validate the `Origin` header against an allowlist before `accept()`, and/or require a token (query-param or first-message token minted from the authenticated HTTP session) at handshake.
48+
- Until an origin check exists, do not rely on cookie/session identity inside WS handlers.
49+
50+
---
51+
52+
## 4. Documentation reality-check
53+
54+
- **CLAUDE.md** claims "CSRF protection required for mutations" — **aspirational, not implemented** for 3 of 4 HTTP adapters; reword to state only Django enforces it and the others require host-level integration.
55+
- **README.md** is accurate: roadmap line 597 "CSRF coverage for FastAPI / Litestar / Flask HTTP paths (today Django-only)"; line 655 "CSRF handling for WebSockets is manual." A4 lands under 0.6.0b.
56+
- **docs/examples/wizard.md:26-27** concedes "the FastAPI adapter has no CSRF" — consistent.
57+
- **examples/django_example** disables CSRF ("for demo purposes," README line 231; `urls_cbv.py` wires `CSRFExemptComponentView`) — models the insecure pattern; add a pointer to production guidance.
58+
- This document is the canonical CSRF/CSWSH reference for the project.
59+
60+
---
61+
62+
## 5. Prioritized follow-up work (issues to be opened)
63+
64+
1. **P1 — `docs: add SECURITY_CSRF.md with per-adapter CSRF/CSWSH guidance`** — land this report as canonical docs; correct CLAUDE.md wording.
65+
2. **P1 — `feat(litestar): document + wire built-in CSRFConfig for component routes`** — highest value-to-effort; framework already has the machinery. Add integration test.
66+
3. **P1 — `feat(websocket): add Origin validation to FastAPI/Litestar WS handshake + document Channels OriginValidator`** — closes the CSWSH gap.
67+
4. **P2 — `docs+feat(fastapi): CSRF integration guide (fastapi-csrf-protect) + optional verify hook`**
68+
5. **P2 — `docs+feat(flask): CSRF integration (Flask-WTF/SeaSurf) for component blueprint`**
69+
6. **P2 — `security: strengthen warnings on Django csrf_exempt variants`** — permit only for non-cookie auth; consider runtime warning.
70+
7. **P3 — `security: reject/normalize form-encoded bodies where JSON is expected`** — form-encoding enables no-preflight cross-origin CSRF; evaluate requiring JSON for the JS client path. Cross-refs roadmap "Cross-adapter request-parse hardening" (README:599).
71+
72+
**Related, kept separate:** unsigned client-round-tripped state (A1/HMAC work) is a distinct integrity gap — CSRF protects *who* can trigger a mutation; state signing protects *what* state they can forge.

0 commit comments

Comments
 (0)