Changelog
v2.1.15 — 2026/06/13
Fix Security
User enumeration via timing attack on login (medium): authenticate_user and DefaultAdminAuth::authenticate returned None immediately via ? when the username was not found in the database, skipping the password hash verification entirely. An attacker measuring response times could distinguish "user does not exist" (fast — no hash work) from "wrong password" (slow — full Argon2 verification), allowing silent username enumeration. Fixed: both functions now always call verify() before short-circuiting. When the user is not found, the password is verified against a pre-computed dummy Argon2 hash (DUMMY_HASH, initialised once at first use via LazyLock) — burning the same CPU time regardless of whether the account exists. The ? on the user lookup is deferred to after verify() returns, so the result is always discarded once timing-sensitive work is done
Fix Security
Missing authorization on the admin reset-password action (medium): in admin_post_id (admin/admin_main/mod.rs), the edit and delete actions were gated by a permission check (can_update / can_delete, with _own + ownership fallback), but the reset-password action had no authorization gate at all — and, unlike admin_get_id, this handler does not check can_access either. Any user authenticated to the admin panel (is_staff or is_superuser) with zero permissions on the resource could POST {prefix}/{resource}/{id}/reset-password to trigger a password-reset for any record, including superuser accounts. CSRF and authentication were enforced, but not authorization. Aggravating factor: when no mailer is configured, the reset link (token + encrypted email) is returned to the calling admin in a flash message — a low-privilege staff user could obtain a working reset link for a higher-privileged account. Fixed: reset-password now requires can_update (global, or can_update_own on an owned record), aligned with edit.
Fix Security
Manual SQL escaping in foreign-key label resolution: the admin generator (admin/daemon/generator.rs) built the FK label lookup queries (list_fn and get_fn) by string-concatenating an IN (...) clause with Expr::cust(format!("CAST(id AS TEXT) IN ({})", ids_csv)), where ids_csv was assembled via format!("'{}'", s.replace('\'', "''")). Escaping only single quotes is insufficient on MySQL/MariaDB, where the backslash is an escape character by default — a value containing a backslash could break out of the literal. In practice the values are DB-stored FK ids (integers), so the exploitability was negligible, but it was manual escaping where the rest of the generator already uses whitelisted identifiers and bound values. Fixed: both queries now use bound values via the typed sea-query API — Expr::cust("CAST(id AS TEXT)").is_in(fk_ids.clone()) in list_fn and .eq(fk_key.clone()) in get_fn. No data is interpolated into the SQL string anymore; the placeholder is backend-correct on PostgreSQL/MySQL/SQLite.
Fix Cli Makemigration
Refacto cli, for all information, see her https://github.com/seb-alliot/runique/blob/main/CHANGELOG.md
Fix runique (forms, admin)
add_js() scripts blocked by CSP strict-dynamic in admin forms: render_js() built its own isolated Tera context containing only js_files, with no access to the request-scoped csp_nonce. Under strict-dynamic, any
Fix runique (forms, admin)
TextField double-encoded & when storing plain-text input: sanitize_strict() used ammonia::Builder::new().tags(HashSet::new()) .clean(input) to strip all HTML tags. This is correct, but ammonia always HTML-encodes & → & in its output even when no tags remain — a side effect of its HTML-entity serialiser. The raw output was then stored in the database. When Tera later rendered it with autoescaping active, the & in & was re-encoded to &, so the browser displayed the literal text & instead of &. Fixed: after ammonia strips the tags, the result is decoded back to plain text via html_escape::decode_html_entities() — removing the spurious entity encoding before storage. Dangerous protocol stripping (javascript:, vbscript:, data:, file:) runs on the decoded string, preserving security guarantees. New dependency: html-escape = "0.2".
Feature runique / derive_form (DSL model! / extend!)
Column rename via [renamed_from: "old"]: renaming a field previously produced a DROP COLUMN + ADD COLUMN — silent data loss. The new explicit hint makes makemigrations emit ALTER TABLE … RENAME COLUMN old TO new (portable across PostgreSQL, MySQL/MariaDB and SQLite), with no data loss. It is a migration-only directive (no effect on the generated entity/form) and is tolerated by both DSL field parsers (FormFieldDecl for v2/extend!, FieldDef for v1). Guard against a stale hint: if the old column still exists in the snapshot, no rename is emitted (falls back to add). ParsedColumn.renamed_from is transient and never written to snapshots.