custom JS removed in favor of proper CSRF implementation
This commit is contained in:
@@ -56,6 +56,22 @@ const COOKIE_MAX_AGE_SECS: i64 = 60 * 60 * 24 * 14;
|
||||
/// fields); anything bigger is rejected rather than buffered.
|
||||
const MAX_BODY_BYTES: usize = 16 * 1024 * 1024;
|
||||
|
||||
tokio::task_local! {
|
||||
/// CSRF token bound to the in-flight request. [`protect`] sets it so the
|
||||
/// `csrf_token()` Tera function can render it into pages (the `hx-headers`
|
||||
/// on `<body>` and the `ui::csrf_field()` hidden input) without every
|
||||
/// controller having to thread it through the view context.
|
||||
static REQUEST_TOKEN: String;
|
||||
}
|
||||
|
||||
/// The CSRF token for the current request task, if one is bound. Returns `None`
|
||||
/// outside a request (e.g. a mailer rendering a template). Used by the
|
||||
/// `csrf_token()` Tera function registered in the view engine.
|
||||
#[must_use]
|
||||
pub fn current_token() -> Option<String> {
|
||||
REQUEST_TOKEN.try_with(String::clone).ok()
|
||||
}
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
fn to_hex(bytes: &[u8]) -> String {
|
||||
@@ -110,9 +126,9 @@ fn forbidden(reason: &str) -> Response {
|
||||
(StatusCode::FORBIDDEN, "CSRF validation failed").into_response()
|
||||
}
|
||||
|
||||
/// Attach a freshly minted token cookie to an outgoing response.
|
||||
fn set_fresh_cookie(res: &mut Response, secret: &str) {
|
||||
let cookie = issue_cookie(make_token(secret));
|
||||
/// Attach the given token to an outgoing response as the `csrf_token` cookie.
|
||||
fn attach_cookie(res: &mut Response, token: &str) {
|
||||
let cookie = issue_cookie(token.to_string());
|
||||
if let Ok(value) = cookie.encoded().to_string().parse() {
|
||||
res.headers_mut().append(header::SET_COOKIE, value);
|
||||
}
|
||||
@@ -171,9 +187,12 @@ pub async fn protect(
|
||||
let exempt = req.uri().path().starts_with("/api/");
|
||||
|
||||
if is_safe || exempt {
|
||||
let mut res = next.run(req).await;
|
||||
// Bind a token for the page to render even on the very first visit, and
|
||||
// persist that same token in the cookie so the later submit matches.
|
||||
let token = cookie_token.clone().unwrap_or_else(|| make_token(&secret));
|
||||
let mut res = REQUEST_TOKEN.scope(token.clone(), next.run(req)).await;
|
||||
if cookie_token.is_none() {
|
||||
set_fresh_cookie(&mut res, &secret);
|
||||
attach_cookie(&mut res, &token);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -185,9 +204,14 @@ pub async fn protect(
|
||||
|
||||
// htmx and other scripted requests send the token as a header; prefer it so
|
||||
// we never have to touch the body.
|
||||
if let Some(header_token) = req.headers().get(CSRF_HEADER).and_then(|v| v.to_str().ok()) {
|
||||
if tokens_match(header_token, &expected) {
|
||||
return next.run(req).await;
|
||||
let header_token = req
|
||||
.headers()
|
||||
.get(CSRF_HEADER)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(str::to_string);
|
||||
if let Some(header_token) = header_token {
|
||||
if tokens_match(&header_token, &expected) {
|
||||
return REQUEST_TOKEN.scope(expected, next.run(req)).await;
|
||||
}
|
||||
return forbidden("CSRF header did not match cookie");
|
||||
}
|
||||
@@ -221,7 +245,8 @@ pub async fn protect(
|
||||
return forbidden("missing or non-matching CSRF form field");
|
||||
}
|
||||
|
||||
next.run(Request::from_parts(parts, Body::from(bytes))).await
|
||||
let req = Request::from_parts(parts, Body::from(bytes));
|
||||
REQUEST_TOKEN.scope(expected, next.run(req)).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user