Threat model
Rat draws a deliberate line between the framework substrate and per-app code. The substrate handles the protocol-level mechanics (CSRF, session signing, header policy, redirect targets), because those would be unreasonable to redo per app. Everything that depends on the deploy environment (TLS, rate limiting, auth flow, logging) lives in author code or in copyable populators. This page lists both sides, so you know where to look when adding a feature.
What the substrate guarantees
Holds without per-app code
Every Rat app inherits the following at the framework level. None of these require populators or settings flags; they fire on the default request path.
CSRF gate on /__rat__/call
HMAC-signed token bound to session id, constant-time compare,
cross-origin rejection on missing or mismatched Origin.
Per-page call manifest
Each rendered page hashes its allowed call surface (functions + __set targets) under the session secret. Cross-page replay
and out-of-manifest calls return 403 before the handler runs.
Session cookie hygiene
__rat_session is HttpOnly, Secure, SameSite=Lax. Server-side
store backs it; only the signed sid travels.
Session id regeneration
session.regenerate() rotates the sid in place. pups/auth.rat
calls it on register, login, and logout to close session
fixation.
Security headers (prod mode)
HSTS 2yr + includeSubDomains, X-Frame-Options SAMEORIGIN,
X-Content-Type-Options nosniff, Referrer-Policy strict-
origin-when-cross-origin, Permissions-Policy denying camera/
mic/geolocation, Server header suppressed, baseline CSP.
Outbound HTTP SSRF guard (prod mode)
http.get/post/put/delete/request refuse loopback, link-local,
RFC 1918, carrier-grade NAT, and non-http(s) schemes. Each
redirect hop is re-validated. Final IP is re-resolved at
dial time to close the DNS-rebinding window.
URL scheme filter on redirect targets
Page-level redirect << "..." rejects javascript:, data:,
vbscript:, and other entity-scheme bypasses.
Path normalisation
/public/../ and percent-encoded traversal variants normalise
before any handler runs.
XFF trust model
req.ip honours X-Forwarded-For only when the immediate peer
is in a trusted-proxy list; direct connections see the raw
remote address.
HEAD / mirrors GET /
The default-route redirect honours HEAD without a body, so
the verb policy stays consistent across canonical paths. What the author wires up
Substrate cannot do it for you
These belong in the deploy story or in copyable populators. The framework intentionally doesn't ship a hosting template: VISION.md keeps the substrate small, and these decisions are too deploy-specific to bake in.
TLS termination
Front the binary with Caddy, nginx, or IIS+ARR. Forward
X-Forwarded-Proto so HSTS engages.
Rate limiting
pups/rate_limit.rat. Token bucket per IP. Tune cap and
refill_per_sec; narrow match: for endpoint-specific gates.
Audit logging
pups/audit_log.rat. JSON line per request, picked up by any
downstream log collector. Broaden or narrow match: as fits.
Authentication
pups/auth.rat. Register, login, logout, current_user. Ships
with timing-equalised user-not-found path and per-account
exponential lockout. Wire your own routes that delegate to
auth.* services.
Brute-force defence beyond the populator
Per-IP token bucket plus per-account lockout cover walk-the-
list and grind-one-account. If you expect botnet-scale
credential stuffing, add a CAPTCHA or proof-of-work gate in
front of login.
Email confirmation
pups/auth.rat ships an email-confirmation flow on register:
new and existing emails both return the identical "if this
email is new, a confirmation link has been sent" payload
after paying a matching bcrypt cycle, closing the register
oracle. Wire SMTP_HOST / SMTP_FROM and set RAT_BASE_URL so
the confirmation link the email carries is absolute.
Secret rotation policy
Rotating RAT_SESSION_SECRET invalidates every existing
session; rotating RAT_ENCRYPTION_KEY locks every [encrypted]
field. Build a runbook before you need it.
Backups
SQLite .backup on a cron, or whatever your durable store
recommends. Framework has no opinion.
Verb allowlist at the edge
Rat dispatches GET / HEAD / POST. The substrate 404s the
rest; the proxy should 405 them so TRACE never reaches the
app at all.
CSP nonces
Prod CSP pins script-src and style-src to a per-request
nonce; 'unsafe-inline' is gone. The security middleware
rolls 128 bits per response, the renderer stamps the
matching nonce on every inline tag it emits, and any
XSS-injected script that isn't carrying the secret is
refused. Static builds ('rat build --prod') emit
inline tags without a nonce - the operator's reverse
proxy owns CSP in that path.
Eval-free runtime
'unsafe-eval' is gone too. Reactive expressions and event
handlers compile to a JSON AST the client runtime walks
with a small interpreter, not to JS strings run through
new Function. So even if an injection reaches a reactive
attribute, there is no eval gate to escalate through - the
handler/expression interpreter only dispatches the fixed
opcode set, never arbitrary code. Out-of-band threats
Not covered by either side
A few classes of risk are outside both the substrate's and the populator's reach. Worth a sentence each so they don't surprise you.
Supply chain
rat install ships a per-file SHA-256 manifest in rat.lock.
Re-pin and re-verify when upgrading; don't run the install
with --ignore-lock in production.
Renderer escaping audit
Bracket interpolation escapes for the four canonical sinks
(text, attr, href, inline-script). User-supplied content
rendered in unusual contexts (SVG, MathML, web components)
has not been audited end-to-end.
Side-channel timing
Substrate paths are not constant-time end-to-end. Crypto
primitives (HMAC compare, bcrypt) are constant-time; routing
and template evaluation are not. Assume an attacker can
observe request timing.
Tail subsystem trust
Python and JS modules under lang/ run with the same trust
as Rat code. Treat them as part of your code review surface. See also Publishing a Rat app · Middleware · Settings