Security¶
Hedron treats escaping, CSRF, cache policy, and redirects as framework boundaries—not optional middleware you remember later.
Profiles¶
Pass security= to Hedron (or build a SecurityPolicy):
| Profile | CSRF | CSP (summary) | Explorer default | Notes |
|---|---|---|---|---|
development |
on | relaxed / unset | may mount | Local iteration; HTMX history cache allowed |
standard |
on | self + limited inline styles | off | Default; HTMX history cache disabled |
strict |
on | no unsafe-inline styles |
off | Requires explicit session_secret; same HTMX hardening |
from hedron import Hedron
app = Hedron(
title="Ops",
security="standard",
session_secret="load-from-secret-store",
explorer="off",
)
HTMX browser preset¶
For standard / strict, PAGE responses inject <meta name="htmx-config"> with
allowEval / allowScriptTags false, historyEnabled false, and historyCacheSize 0 (plus
existing restore/selfRequests defaults). development keeps eval/scripts disabled but does not
wipe history cache. Opt out with SecurityPolicy(..., htmx_browser_preset=False) or own the full
meta yourself. Inspect with SecurityPolicy.htmx_config_json().
Python hx-vals / hx-headers js:¶
html.* rejects js: expressions on hx-vals / hx-headers by default (HED-SEC-0011), matching
HDJ htmx.eval. Opt in with allow_htmx_eval() or SecurityPolicy(allow_htmx_eval=True). JSON
object literals without js: remain allowed.
See Security types for boundary types (SafeUrl, …).
HDJ templates¶
HDJ files are trusted application source, like Python modules and JavaScript files. They may use native HTML, CSS, JavaScript, Web Components, Jinja, and HTMX directly. Strict HDJ mode protects dynamic values and component/view contracts; it does not redefine HTML.
Before serving a template, HDJ compares its capability report with the active security and asset
policy. Inline script/style, HTMX eval or response-script processing, remote origins, and
extensions must be allowed deliberately. Hedron never adds unsafe-inline, unsafe-eval, remote
origins, or a nonce automatically. Prefer registered modules/styles under strict CSP. Template
authors remain trusted; do not load tenant, CMS, prompt, upload, or database text as executable HDJ.
CSRF¶
When CSRF is enabled (all built-in FastAPI profiles):
- Safe GET responses may set the CSRF cookie (
hedron_csrfby default). - Unsafe methods on page/component/action routes (including
include_componentwhen POST is declared) require a matchingX-CSRF-Tokenheader orcsrf_tokenform field. - On HTTPS, the CSRF cookie is marked
Secure. UnderHEDRON_ENV=production/prod(and FlaskFLASK_ENV/ENV=production), Secure is forced even when the app sees plain HTTP behind a TLS terminator.
Flask adapter: hedron_route and HedronFlask.respond validate the same double-submit cookie
for unsafe methods (auto cookie issuance on safe GETs remains on by default).
Django adapter: CsrfViewMiddleware remains authoritative. Safe GETs through
HedronDjango.respond / hedron_view call get_token so the CSRF cookie is seeded. For
portable clients that send X-CSRF-Token, set CSRF_HEADER_NAME = "HTTP_X_CSRF_TOKEN".
Form posts must use csrfmiddlewaretoken (Django middleware). Bare CsrfField() under
the Django adapter emits that field name via RenderContext; the portable FastAPI/Flask
default csrf_token is not accepted by CsrfViewMiddleware.
Phase 0.22 — CSRF composition
Pluggable CSRF strategies (SessionTokenCsrf, DoubleSubmitCookieCsrf), composable
SecurityHeadersPolicy merge/override, and CsrfField / Form(hx=Hx(...)) ship on
0.22. See CSRF composition and
RELEASE_0_22. Named profiles keep Compatible
double-submit defaults; seed tokens with csrf_token_for_request or use CsrfField
as below.
Seed the cookie with a GET against a real page route, then POST an action or component
route that requires CSRF. Example using the
HTMX interactions sample (/ seeds; /status is GET-only—add a
POST action for writes):
Try it (simulated)¶
POST with CSRF succeeds; missing token → 403. Docs simulation.
Docs simulation — canned success vs 403 routes (not a live CSRF cookie check).
Minimal runnable app.py that reproduces this demo (real Hedron, not the docs simulator):
import os
from fastapi import Request
from hedron import (
CsrfField,
Form,
Hedron,
Hx,
Page,
Stack,
SubmitButton,
Text,
csrf_token_for_request,
)
app = Hedron(
title="CSRF demo",
security="standard",
explorer="off",
session_secret=os.environ.get("HEDRON_SESSION_SECRET", "dev-only"),
)
def _csrf(request: Request) -> str:
return csrf_token_for_request(request, request.app.state.hedron_security)
@app.page("/")
def home(request: Request) -> Page:
token = _csrf(request)
return Page(
Stack(
Text("GET seeds hedron_csrf"),
Form(
CsrfField(token=token),
SubmitButton("POST with CSRF"),
action="/do",
method="post",
hx=Hx(target="body", swap="outerHTML"),
),
Form(
SubmitButton("POST without CSRF"),
action="/do",
method="post",
),
),
title="CSRF",
)
@app.action("/do", method="POST")
def do_action() -> Page:
return Page(Text("POST ok"), title="Done")
from hedron import Hedron, Page, Text
app = Hedron(title="CSRF demo", security="standard", session_secret="replace-me")
@app.page("/")
def home() -> Page:
return Page(Text("GET seeds hedron_csrf"), title="Home")
@app.action("/do")
def do_action() -> Page:
return Page(Text("POST ok"), title="Done")
# Terminal: uv run uvicorn app:app --reload
curl -c jar -b jar http://127.0.0.1:8000/
TOKEN=$(grep hedron_csrf jar | awk '{print $NF}')
curl -b jar -H "X-CSRF-Token: $TOKEN" -X POST http://127.0.0.1:8000/do
Without the header, expect 403. Safe GETs alone do not require the token.
Redirects and HTMX headers¶
redirect_local("/path")accepts only local paths (rejects//…and\open-redirect forms).redirect_external(...)fails closed unless the policy setsallow_external_redirects=True.- Approved HTMX headers (
HX-Redirect,HX-Push-Url, …) must use local paths. - HTMX target and reselect values must use Hedron's safe selector subset.
- Route
fragment_regionsallowlists reject an unauthorizedHX-Targetwith403. InteractionResult.headerscannot introduce arbitrary response headers; approved names are re-validated through the same URL and selector checks as declared fields.
Use InteractionResult(redirect=..., retarget=..., cache=...) instead of constructing
raw HX-* headers when a declared field exists. See the
HTMX interaction guide.
Explorer modes¶
| Mode | Behavior |
|---|---|
off |
Not mounted |
development |
Mounted for local use; forced off in production |
secured |
Mounted behind explorer_dependencies / auth gate |
Prefer explorer="off" in scaffolds and production. Install hedron[dev] when you need
Explorer, then open /hedron-explorer/ (trailing slash) on the running app.
Markdown sanitize and chart callbacks¶
Markdownrenders throughTrustedHtml.nh3— installhedron[markdown]/hedron[sanitize]. Do not pass unsanitized HTML throughhtml.rawwithout an explicit trust boundary (TrustedHtml.reviewedorTrustedHtml.nh3).- Chart adapters reject raw JavaScript callbacks and unapproved remote CDN assets. Browser runtimes are pinned and served locally under Hedron static paths.
See Content, Charts, and Security types.