Releases and security
2.1.21 - 2026-06-30
Security and robustness audit from a full reverse-engineering of the framework into UML/Merise diagrams (
diagramme/folder). Several false positives were dismissed after verification (makemigrations does handleALTER COLUMN, the error page is gated ondebug, the error handler is mounted by default).
Breaking — runique (CSRF fail-closed: raw body access removed)
Prisme::datais nowpub(crate)(waspub). Readingreq.prisme.datadirectly let user code consume the request body bypassing CSRF validation (anomaly C2) — the framework never did this internally (admin CRUD, login andreq.form()all gate CSRF before touching the body), but third-party handlers could. The body is now reachable from a handler only throughreq.prisme.checked_data()(returnsSomeonly when CSRF is valid,Noneotherwise — for API/AJAX endpoints that read the body without an HTML form) orreq.form(). Migration: replacereq.prisme.data.get("x")withreq.prisme.checked_data().and_then(|d| d.get("x").cloned()).
Security — runique (uploads committed before any check)
- Uploaded files were moved straight into
MEDIA_ROOTduring body parsing — i.e. before CSRF validation, before form validation, and without extension checks (unauthenticated file write on any public route, disk fill, possibly-served content).parse_multipartnow writes to a staging dir.staging-{uuid}(not served);FileField::finalizeis the sole committer (it also handles the no-upload_tocase by moving toMEDIA_ROOTroot and storing a relative path);sweep_stale_stagingpurges staging dirs orphaned by a rejected request (failures logged, never swallowed).
Security — runique (hardened headers never set + dead STRICT_CSP)
STRICT_CSP(env, defaulttrue) was read but never consumed, andenable_header_securitywas alwaysfalse→ no hardened security headers (HSTS, X-Frame-Options, COOP/CORP), even in ACME mode where Runique terminates TLS.strict_cspnow drivesenable_header_security(secure-by-default; the.with_csp(...)builder still wins). HSTS is only emitted when Runique actually serves HTTPS (enforce_httpsoracme_enabled, viaSecurityConfig::should_emit_hsts) — avoids a one-year HTTPS lock-in on an HTTP deployment.
Security — runique (empty/weak SECRET_KEY accepted in production)
- Boot validation rejected the default key but not an empty key (
SECRET_KEY="") nor a too-short one → CSRF/HMAC silently broken.cross_validatenow refuses to start in production for any empty, default, or < 32-character key (secret_key_is_weak, HMAC-SHA256 floor). In debug: warning only.
Security — runique (SMTP password in clear in logs)
MailerConfigderivedDebug, so{:?}(in a log) printed the SMTP password in clear. ManualDebugimpl now maskspassword: "***".
Fix — runique (inconsistent authenticated session duration)
login()hardcoded 24h, overriding the value set viawith_session_duration(...)that the refresh middleware (session_ttl_upgrade) already uses → cookie,eihwaz_sessionsrow and refresh diverged on the first request. Single source now:loginreads the builder value (via anOnceLockset at build), 24h default if unset, warning if re-set to a different value.
Fix — runique (incompatible FK under the big-pk feature)
eihwaz_users_groupes.user_idwas hardcodedINTEGERwhileeihwaz_users.idbecomesBIGINTunderbig-pk→ type-incompatible foreign key (migration failure on strict Postgres). Aligned with the other tables' conditionalbig-pkblock.
Fix — runique (silently swallowed errors — "zero swallowed errors" sweep)
- Several failing
Results were ignored (let _ =,if let Okwithoutelse) and are now traced: form transaction rollback,eihwaz_historyaudit insert, user→group assignment (RBAC), file and reset-token cleanup, honeypot session insert, reset email template render, poisoned permission-cache lock (now recovered viainto_innerinstead of skipped).
Feature — runique (fail-closed CSRF accessor)
Prisme::checked_data(): returns body data only when CSRF is valid, for handlers that would readreq.prisme.datadirectly without going throughreq.form().- CSRF per-method policy collapsed into a single source of truth (
csrf_required): GET/HEAD are exempt, every other method (POST/PUT/PATCH/DELETE, but also OPTIONS/TRACE/unknown) requires a valid token (fail-closed). The pipeline andRequest::form()now share it so they cannot drift. Behavior unchanged.
Fix — runique (clean 503 on session-store saturation instead of a generic 500)
- When the in-memory session store hit its high watermark, a new login's session
createwas refused by the store (typed error, logged) but tower surfaced it at response-commit time as a generic 500. AddedCleaningMemoryStore::is_saturated()andRuniqueEngine::session_store_saturated()so a login handler can fail fast and clean: the admin login now returns 503 Service Unavailable with aRetry-Afterheader (rendered via a new503.htmltemplate +render_503, translated in the 9 locales). Public login handlers can guard the same way viaengine.session_store_saturated().
Fix — runique (phantom eihwaz_droits table)
- The legacy single
eihwaz_droitsrights table (superseded by the per-groupeihwaz_groupes_droitsmatrix) survived as dead weight: an orphan entity file (not declared inpermissions/mod.rs, never compiled, no migration) and an entry in theextend!{}framework-table whitelist + phantom-column registry — soextend!{ table: "eihwaz_droits", … }would have been accepted at macro time and then failed at migration runtime against a table that does not exist. Removed on all fronts (entity file,FRAMEWORK_TABLES,EIHWAZ_DROITSregistry static/arm, misleading comment).
Misc — runique
ColumnDef::to_form_fieldlogs (debug level) when an unhandled column type falls back to a default text field.- Removed an unreachable duplicate
(model)arm inimpl_form_access!(the second copy lacked thecustomizehook call and could never match —macro_rules!picks the first arm). - Session docs (fr/en): the authenticated session duration is explicitly documented (single source, builder-only, Django
SESSION_COOKIE_AGEequivalent, example, and a "remember me" pattern via per-sessionset_expiry).