feat: server-side CSRF token validation
Introduces CsrfForm<T>: a FromRequest extractor that looks up the session's csrf_token and constant-time-compares it to a csrf_token field in the form body. Drop-in replacement for Form<T> on every state-changing /web/ POST handler. Missing or expired session → redirect to /web/login; malformed body → 400; token mismatch → 403. WebSession carries csrf_token, minted alongside the session token at login and magic-link consume. Not rotated per request — rotation would break multi-tab use and buys little over SameSite=Strict cookies. Handlers: bulk, purge-closed, reset, close, reopen, analyze, delete-recording, logout all now require CsrfForm<_>. Login stays unprotected (SameSite=Strict alone is sufficient — forced-login CSRF has no impact on this codebase). /api/... is header-auth, exempt. Constant-time compare via subtle::ConstantTimeEq avoids timing oracles on the token. Template work is NOT in this commit — browser forms still post without csrf_token, so the live web UI will 403 until Schritt 6 (templates render the hidden field). Tests: all 12 red specs in csrf_attack_test.rs now green, #[ignore] removed. Integration tests that POSTed to protected endpoints switched to a new create_router_and_session_store entrypoint which returns a handle to the store so tests can read csrf_token for the active session.
This commit is contained in:
@@ -222,7 +222,7 @@ async fn case_detail_of_foreign_case_returns_404() {
|
||||
#[tokio::test]
|
||||
async fn logout_clears_session() {
|
||||
let config = test_config_with_users(vec![make_user("dr_a", "s")]);
|
||||
let app = doctate_server::create_router(config);
|
||||
let (app, store) = doctate_server::create_router_and_session_store(config);
|
||||
|
||||
let login = app
|
||||
.clone()
|
||||
@@ -230,6 +230,8 @@ async fn logout_clears_session() {
|
||||
.await
|
||||
.unwrap();
|
||||
let cookie = extract_session_cookie(&login).unwrap();
|
||||
let token = cookie.trim_start_matches("session=");
|
||||
let csrf = store.read().await.get(token).unwrap().csrf_token.clone();
|
||||
|
||||
let logout = app
|
||||
.clone()
|
||||
@@ -238,7 +240,8 @@ async fn logout_clears_session() {
|
||||
.method("POST")
|
||||
.uri("/web/logout")
|
||||
.header(header::COOKIE, &cookie)
|
||||
.body(Body::empty())
|
||||
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
|
||||
.body(Body::from(format!("csrf_token={csrf}")))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user