refactor: drop /web/ URL prefix from browser routes

The /web/ prefix predated the /api/ split; today it just clutters every URL
without disambiguating anything. All 16 browser routes move to the apex
(/cases, /login, /magic, /events, /audio/...). The 6 /api/* routes are
unchanged. A new /->>/cases redirect closes the apex 404.

The open-redirect guard in magic.rs and case_actions.rs flips from a
positive whitelist (starts_with("/web/")) to a deny-list: same-origin path,
not protocol-relative, not under /api/, no \. The /api/ exclusion is now
load-bearing and covered by tests.

Pre-production: no transition redirects.
This commit is contained in:
2026-05-04 18:36:10 +02:00
parent 3d67cbc1c8
commit 2c6062a53e
43 changed files with 313 additions and 266 deletions
+5 -6
View File
@@ -1,4 +1,4 @@
//! Attack-confirming tests for CSRF protection on /web/ state-changing
//! Attack-confirming tests for CSRF protection on / state-changing
//! endpoints.
//!
//! Each test crafts a request that simulates a real attack shape
@@ -185,7 +185,7 @@ async fn delete_recording_without_csrf_forbidden() {
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
}
/// Forced-logout CSRF: attacker page auto-POSTs /web/logout to sign the
/// Forced-logout CSRF: attacker page auto-POSTs /logout to sign the
/// victim out of their active session (annoyance / phishing setup where
/// victim re-enters password on a lookalike page).
#[tokio::test]
@@ -334,18 +334,17 @@ async fn my_cases_page_renders_csrf_token_hidden_field() {
let (app, store) = doctate_server::create_router_and_session_store(cfg);
let (cookie, csrf) = login_with_csrf(&app, &store, "dr_admin", "s").await;
let body = html_body(&app, "/web/cases?show_closed=1", &cookie).await;
let body = html_body(&app, "/cases?show_closed=1", &cookie).await;
let expected = hidden_field(&csrf);
assert!(
body.contains(&expected),
"/web/cases should render hidden csrf_token ({expected}), \
"/cases should render hidden csrf_token ({expected}), \
first 200 chars of body: {}",
&body[..body.len().min(200)]
);
assert!(
body.contains(r#"action="/web/logout""#)
&& body.matches(r#"name="csrf_token""#).count() >= 2,
body.contains(r#"action="/logout""#) && body.matches(r#"name="csrf_token""#).count() >= 2,
"expected at least 2 csrf_token hidden fields (logout + bulk); \
got {} occurrences",
body.matches(r#"name="csrf_token""#).count()