Changelog
v2.1.12 — 2026/05/30
Fix Security
CSRF token computed but never enforced on public forms (critical): the Prisme pipeline computes csrf_valid for every mutating request but returns it as a flag without rejecting. The form layer (Request::form() / is_valid()) never consumed it, and the CSRF hidden-field validator is a no-op since set_expected_value was removed (masked tokens differ per request). Only the admin panel re-checked the flag manually, so any public POST handler built on the documented form.is_valid() pattern accepted cross-site forged submissions — verified end-to-end against a live registration endpoint (an account was created with no session cookie and no token). Fixed: Request::form() now sets force_invalid when the request is mutating and prisme.csrf_valid is false, so is_valid() fails closed — reusing the existing honeypot mechanism without reintroducing set_expected_value
Fix exclusive_login
exclusive_login only invalidated memory sessions, not DB sessions: CleaningMemoryStore::save() evicted in-memory sessions for the same user when exclusive_login = true, but never called RuniqueSessionStore of invalidate_other_sessions(). After a server restart, the evicted sessions were restored from DB, making the exclusive-login guarantee ineffective. Fixed: the DB invalidation is now collected inside the lock and executed after release, symmetric with the memory cleanup. Using Pk directly (serde_json::from_value::) instead of as_i64 — correct under the big-pk feature flag.
Fix SQL injection on MySQL/MariaDB via raw-SQL value interpolation
admin list filters, search (search_cond!), bulk group_set, and the m2m option query built conditions with Expr::cust(format!("... = '{}'", val)) escaped only by doubling single quotes (' → ''). This is sufficient on PostgreSQL/SQLite (standard_conforming_strings) but bypassable on MySQL/MariaDB, where a backslash escapes the following quote (\' followed by '' breaks out of the string literal). An authenticated staff user with read access could execute arbitrary SQL. Fixed: all attacker-controlled values are now bound parameters via Expr::cust_with_values(..., [val]), delegating escaping to sea-query's backend-aware layer. Column identifiers stay inline but remain whitelisted (FILTER_COLS / SORT_COLS) or schema-fixed
Fix SQL injection in built-in admin resources
the hand-written list_fn of the built-in users, groupes, and droits resources interpolated the filter column name (?filter_=) directly into CAST({col} AS TEXT) with no whitelist — an identifier injection exploitable on every backend, not just MySQL — in addition to the unsafe value escaping. Fixed: column names are validated against an [A-Za-z0-9_] charset before use, and values are bound via cust_with_values
Fix Stored XSS via the | markdown filter
the template preprocessor rewrites every {{ x | markdown }} to {{ x | markdown | safe }}, and the filter emitted pulldown-cmark output without sanitization. Raw inline HTML (
Fix Open-redirect filter bypass via backslash
is_safe_redirect treated /\evil.com as a safe relative path (starts_with('/') but not "//"). Browsers normalize \ to /, turning it into the protocol-relative //hack.com. Fixed: backslashes are normalized to forward slashes before the same-origin determination.
Fix Use acme
IP spoofing via X-Forwarded-For in standalone-TLS mode : the built-in TLS server (axum_server::bind_rustls, used for ACME / standalone HTTPS) served the router via into_make_service() without connect-info. With no ConnectInfo, trusted_proxies saw conn_ip = None, defaulted to loopback (a trusted CIDR), and therefore honored the client-controlled X-Forwarded-For header — letting any client forge its IP (rate-limit bypass, forged audit logs). Fixed on three layers: (1) the TLS serve path now uses into_make_service_with_connect_info::(), exposing the real peer IP; (2) extract_client_ip returns loopback without ever reading X-Forwarded-For when the peer IP is unknown, so a missing connect-info can no longer enable spoofing; (3) IPv4-mapped IPv6 peers and XFF entries (::ffff:a.b.c.d, seen on dual-stack sockets) are canonicalized to IPv4 before the trusted-CIDR check, so a private reverse proxy is correctly recognized. Covered by unit tests in trusted_proxies.rs
Fix is_authenticated
deserialized the user id as i32 (low): under the big-pk feature (i64), a user id exceeding i32::MAX failed to deserialize, so is_authenticated returned false inconsistently with get_user_id (which uses Pk). Fixed: it now reads Pk