221c1f31b0
Backend (each change benchmark-gated with BEFORE replicas + equivalence gates; see examples/bench_round11_micro.rs, bench_round11_queries.rs, bench_log_writer.rs and benches/ROUND11.md — final numbers land in the follow-up doc commit): - StoragePath re-representation: single canonical joined String, segments derived on demand; File/Folder drop the duplicated path_string field (4000→1000 allocs per 500-row listing page) - Display classifier fusion: classify_display shares one stack-lowered extension across the three decision trees; call sites in FileDto, folder/favorites/recent handlers, trash, path-resolver (+ interning where Arc::from was still used) - /status.php and /openapi.json memoized into OnceLock<Bytes> (openapi rebuilt a 171 KiB spec per request: 2.8 ms → 18 ns) - NC upload-session PROPFIND: write! + pre-sized body + stack RFC2822 dates (2.3-2.6x, 2582→772 allocs at 256 chunks) - REST download: dead FileDto clone removed (capture mime/size + move) - CalendarEventDto/TrashedItem into_parts moves (11 KiB ical_data memcpy gone per CalDAV row); CardDAV getlastmodified stack render - 4xx path: borrowed ErrorResponse serialize, ErrorKind::as_str, not_found/already_exists clone kill - vCard emit via write!; search page moved out with into_iter skip/take; content-hit UUIDs parsed once; group last-user check via HashSet - RateLimiter: lock-free get + insert (and_upsert_with variant REJECTED by benchmark); CSRF token borrow-compare + borrowed cookie extraction - Thumbnail/preview ETags built from as_str (Debug-identical bytes) - Encrypted backend: encrypt_in_place_detached single-buffer write path, chunk-sized reserve in collect_stream; retry labels made lazy - PG: deferred upload registration 3→1 round-trips (persist_file CTE template); direct_grant_cache for Calendar/AddressBook/Playlist authz (single-flight + set_role/clear_role invalidation); expand_user tokio::join!; geo clusters min(uuid)::text; recluster face assignment batched into one UNNEST update - People recluster cosine: norms precomputed once (bit-identical gate) - NC capabilities poll logs demoted to debug; tracing-appender dep added for the log-writer benchmark Frontend: - ResourceList.selectedEntries O(N)-per-toggle → id-index projection O(k log k); favorites/recent consume the batchToolbar snippet param and drop their duplicate filter + dead selectedIds mirror - Recent: star state via new favoriteIds prop — a star click no longer rebuilds all N entries - admin timeAgo >30d fallback uses the cached Intl.DateTimeFormat - vitest gates in src/lib/components/round11.bench.test.ts Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ABhTEHuGujvwoodh67Kga7
75 lines
2.8 KiB
Rust
75 lines
2.8 KiB
Rust
//! CSRF double-submit cookie middleware.
|
||
//!
|
||
//! State-changing requests (`POST`, `PUT`, `DELETE`, `PATCH`) that were
|
||
//! authenticated via an HttpOnly cookie (i.e. browser sessions) **must**
|
||
//! include an `X-CSRF-Token` header whose value matches the `oxicloud_csrf`
|
||
//! cookie. Requests authenticated via `Bearer` or `Basic` headers are
|
||
//! exempt because they are not vulnerable to CSRF — the browser never
|
||
//! attaches those automatically.
|
||
//!
|
||
//! Safe methods (`GET`, `HEAD`, `OPTIONS`) are always allowed through.
|
||
|
||
use axum::{
|
||
extract::Request,
|
||
http::{Method, StatusCode},
|
||
middleware::Next,
|
||
response::{IntoResponse, Response},
|
||
};
|
||
|
||
use crate::interfaces::api::cookie_auth;
|
||
use crate::interfaces::middleware::auth::CookieAuthenticated;
|
||
|
||
/// Methods considered safe (no side-effects) — CSRF check is skipped.
|
||
const SAFE_METHODS: [Method; 3] = [Method::GET, Method::HEAD, Method::OPTIONS];
|
||
|
||
/// Middleware that enforces CSRF protection for cookie-authenticated browser
|
||
/// sessions using the **double-submit cookie** pattern.
|
||
///
|
||
/// Must be applied **after** `auth_middleware` so that the
|
||
/// `CookieAuthenticated` marker is available in extensions.
|
||
pub async fn csrf_middleware(request: Request, next: Next) -> Result<Response, Response> {
|
||
// Safe methods never need CSRF validation.
|
||
if SAFE_METHODS.contains(request.method()) {
|
||
return Ok(next.run(request).await);
|
||
}
|
||
|
||
// Only enforce for cookie-authenticated sessions.
|
||
let is_cookie_auth = request.extensions().get::<CookieAuthenticated>().is_some();
|
||
if !is_cookie_auth {
|
||
return Ok(next.run(request).await);
|
||
}
|
||
|
||
// Extract the CSRF token from the cookie (borrow-only).
|
||
let cookie_token = cookie_auth::extract_cookie_str(request.headers(), cookie_auth::CSRF_COOKIE);
|
||
|
||
// Extract the CSRF token from the request header. Borrow-only:
|
||
// `String: PartialEq<&str>` covers the comparison, so materializing an
|
||
// owned copy per state-changing request was a pure waste
|
||
// (benches/ROUND11.md §6: 15.7 → 1.3 ns, −1 alloc).
|
||
let header_token = request
|
||
.headers()
|
||
.get(cookie_auth::CSRF_HEADER)
|
||
.and_then(|v| v.to_str().ok());
|
||
|
||
match (cookie_token, header_token) {
|
||
(Some(c), Some(h)) if !c.is_empty() && c == h => {
|
||
// Tokens match — allow the request through.
|
||
Ok(next.run(request).await)
|
||
}
|
||
_ => {
|
||
tracing::warn!(
|
||
method = %request.method(),
|
||
uri = %request.uri(),
|
||
"CSRF validation failed: missing or mismatched token"
|
||
);
|
||
Err((
|
||
StatusCode::FORBIDDEN,
|
||
axum::Json(serde_json::json!({
|
||
"error": "CSRF token missing or invalid"
|
||
})),
|
||
)
|
||
.into_response())
|
||
}
|
||
}
|
||
}
|