1pub use crate::shared::csrf::{CsrfError, CsrfErrorKind, CsrfTokenNames, RequestSourceMode};
3use axum::{
4 extract::{Request, State},
5 middleware::Next,
6 response::Response,
7};
8use http::{HeaderMap, header};
9use std::sync::Arc;
10
11#[derive(Clone)]
13pub struct RequestSourcePolicy {
14 pub scheme: String,
16 pub host: String,
18 pub public_site_origins: Vec<String>,
20 pub mode: RequestSourceMode,
22}
23
24pub fn ensure_double_submit_token_with_names(
29 headers: &HeaderMap,
30 names: &CsrfTokenNames,
31) -> crate::shared::csrf::Result<()> {
32 let cookie = headers
33 .get_all(header::COOKIE)
34 .iter()
35 .filter_map(|value| value.to_str().ok())
36 .flat_map(|value| value.split(';'))
37 .filter_map(|value| cookie::Cookie::parse_encoded(value.trim()).ok())
38 .find(|cookie| cookie.name() == names.cookie_name());
39 crate::shared::csrf::ensure_token_values(
40 cookie.as_ref().map(cookie::Cookie::value),
41 headers
42 .get(names.header_name())
43 .and_then(|value| value.to_str().ok()),
44 )
45}
46
47pub fn ensure_double_submit_token(headers: &HeaderMap) -> crate::shared::csrf::Result<()> {
52 ensure_double_submit_token_with_names(headers, crate::shared::csrf::default_csrf_token_names())
53}
54
55pub fn ensure_request_source_allowed(
61 headers: &HeaderMap,
62 policy: &RequestSourcePolicy,
63) -> crate::shared::csrf::Result<()> {
64 let origin = crate::shared::csrf::request_origin(&policy.scheme, &policy.host)?;
65 let value = |name| {
66 headers
67 .get(name)
68 .map(|value| value.to_str().unwrap_or("\u{fffd}"))
69 };
70 crate::shared::csrf::ensure_headers_allowed(
71 value("origin"),
72 value("referer"),
73 value("sec-fetch-site"),
74 &origin,
75 &policy.public_site_origins,
76 policy.mode,
77 )
78}
79
80type Predicate = dyn Fn(&Request) -> bool + Send + Sync;
81type SourceResolver = dyn Fn(&Request) -> Result<RequestSourcePolicy, Box<Response>> + Send + Sync;
82type ErrorMapper = dyn Fn(CsrfError) -> Response + Send + Sync;
83
84#[derive(Clone)]
86pub struct CsrfConfig {
87 names: CsrfTokenNames,
88 protect: Arc<Predicate>,
89 source: Arc<SourceResolver>,
90 map_error: Arc<ErrorMapper>,
91}
92
93impl CsrfConfig {
94 pub fn new<P, S, M>(names: CsrfTokenNames, protect: P, source: S, map_error: M) -> Self
96 where
97 P: Fn(&Request) -> bool + Send + Sync + 'static,
98 S: Fn(&Request) -> Result<RequestSourcePolicy, Box<Response>> + Send + Sync + 'static,
99 M: Fn(CsrfError) -> Response + Send + Sync + 'static,
100 {
101 Self {
102 names,
103 protect: Arc::new(protect),
104 source: Arc::new(source),
105 map_error: Arc::new(map_error),
106 }
107 }
108}
109
110pub async fn csrf(State(config): State<CsrfConfig>, request: Request, next: Next) -> Response {
112 if crate::shared::csrf::is_unsafe_method(request.method()) && (config.protect)(&request) {
113 let policy = match (config.source)(&request) {
114 Ok(policy) => policy,
115 Err(response) => return *response,
116 };
117 let result = ensure_request_source_allowed(request.headers(), &policy)
118 .and_then(|()| ensure_double_submit_token_with_names(request.headers(), &config.names));
119 if let Err(error) = result {
120 return (config.map_error)(error);
121 }
122 }
123 next.run(request).await
124}
125
126#[cfg(test)]
127mod tests {
128
129 use super::{CsrfConfig, CsrfTokenNames, RequestSourceMode, RequestSourcePolicy, csrf};
130 use axum::{
131 Router,
132 body::{Body, to_bytes},
133 middleware,
134 response::IntoResponse,
135 routing::any,
136 };
137 use http::{HeaderValue, Request, StatusCode};
138 use tower::ServiceExt;
139
140 fn policy() -> RequestSourcePolicy {
141 RequestSourcePolicy {
142 scheme: "https".into(),
143 host: "api.example.com".into(),
144 public_site_origins: vec!["https://panel.example.com".into()],
145 mode: RequestSourceMode::Required,
146 }
147 }
148
149 #[tokio::test]
150 async fn product_source_resolver_failure_is_returned_unchanged() {
151 let config = CsrfConfig::new(
152 CsrfTokenNames::default(),
153 |_| true,
154 |_| {
155 Err(Box::new(
156 (StatusCode::SERVICE_UNAVAILABLE, "source policy unavailable").into_response(),
157 ))
158 },
159 |_| StatusCode::FORBIDDEN.into_response(),
160 );
161 let router = Router::new()
162 .fallback(any(|| async { "called" }))
163 .layer(middleware::from_fn_with_state(config, csrf));
164 assert_eq!(
165 result(
166 router,
167 Request::builder()
168 .method("POST")
169 .body(Body::empty())
170 .unwrap()
171 )
172 .await,
173 (
174 StatusCode::SERVICE_UNAVAILABLE,
175 "source policy unavailable".into()
176 )
177 );
178 }
179
180 fn app(names: CsrfTokenNames, source: RequestSourcePolicy) -> Router {
181 let config = CsrfConfig::new(
182 names,
183 |req| req.uri().path() != "/public",
184 move |_| Ok(source.clone()),
185 |error| (StatusCode::FORBIDDEN, format!("{:?}", error.kind())).into_response(),
186 );
187 Router::new()
188 .fallback(any(|| async { "called" }))
189 .layer(middleware::from_fn_with_state(config, csrf))
190 }
191
192 async fn result(app: Router, request: Request<Body>) -> (StatusCode, String) {
193 let response = app.oneshot(request).await.unwrap();
194 let status = response.status();
195 let body = to_bytes(response.into_body(), 4096).await.unwrap();
196 (status, String::from_utf8(body.to_vec()).unwrap())
197 }
198
199 #[tokio::test]
200 async fn safe_methods_and_product_exempt_routes_bypass_checks() {
201 for method in ["GET", "HEAD", "OPTIONS", "TRACE"] {
202 let (status, _) = result(
203 app(CsrfTokenNames::default(), policy()),
204 Request::builder()
205 .method(method)
206 .uri("/")
207 .body(Body::empty())
208 .unwrap(),
209 )
210 .await;
211 assert_eq!(status, StatusCode::OK, "{method}");
212 }
213 for method in ["POST", "PATCH", "PUT", "DELETE"] {
214 let router = app(CsrfTokenNames::default(), policy());
215 let (status, body) = result(
216 router.clone(),
217 Request::builder()
218 .method(method)
219 .body(Body::empty())
220 .unwrap(),
221 )
222 .await;
223 assert_eq!(
224 (status, body.as_str()),
225 (StatusCode::FORBIDDEN, "RequestSourceMissing")
226 );
227 let (status, _) = result(
228 router,
229 Request::builder()
230 .method(method)
231 .uri("/public")
232 .body(Body::empty())
233 .unwrap(),
234 )
235 .await;
236 assert_eq!(status, StatusCode::OK);
237 }
238 }
239
240 #[tokio::test]
241 async fn token_failures_are_mapped_before_the_handler_runs() {
242 for (cookie, header, kind) in [
243 (None, None, "CookieMissing"),
244 (Some("aster_csrf=token"), None, "HeaderMissing"),
245 (Some("aster_csrf=token"), Some(""), "HeaderMissing"),
246 (Some("aster_csrf=token"), Some(" "), "HeaderMissing"),
247 (Some("aster_csrf="), Some("token"), "TokenInvalid"),
248 (Some("aster_csrf=token"), Some("wrong"), "TokenInvalid"),
249 (
250 Some("aster_csrf=token"),
251 Some("longer-token"),
252 "TokenInvalid",
253 ),
254 (Some("aster_csrf=token"), Some(" token "), "called"),
255 ] {
256 let mut req = Request::builder()
257 .method("POST")
258 .header("origin", "https://api.example.com");
259 if let Some(cookie) = cookie {
260 req = req.header("cookie", cookie);
261 }
262 if let Some(header) = header {
263 req = req.header("x-csrf-token", header);
264 }
265 let (status, body) = result(
266 app(CsrfTokenNames::default(), policy()),
267 req.body(Body::empty()).unwrap(),
268 )
269 .await;
270 assert_eq!(body, kind);
271 assert_eq!(
272 status,
273 if kind == "called" {
274 StatusCode::OK
275 } else {
276 StatusCode::FORBIDDEN
277 }
278 );
279 }
280 }
281
282 #[tokio::test]
283 async fn custom_names_and_encoded_multi_cookie_headers_work() {
284 let names = CsrfTokenNames::new("product_csrf", "X-Product-CSRF").unwrap();
285 let router = app(names, policy());
286 let request = Request::builder()
287 .method("PATCH")
288 .header("origin", "https://panel.example.com")
289 .header("cookie", "other=value; invalid")
290 .header("cookie", "product_csrf=token%2Da")
291 .header("X-Product-CSRF", "token-a")
292 .body(Body::empty())
293 .unwrap();
294 assert_eq!(
295 result(router.clone(), request).await,
296 (StatusCode::OK, "called".into())
297 );
298 let request = Request::builder()
299 .method("PATCH")
300 .header("origin", "https://api.example.com")
301 .header("cookie", "aster_csrf=token-a")
302 .header("X-CSRF-Token", "token-a")
303 .body(Body::empty())
304 .unwrap();
305 assert_eq!(result(router, request).await.1, "CookieMissing");
306 }
307
308 #[tokio::test]
309 async fn source_origin_referer_priority_and_fetch_metadata_match_actix() {
310 for (origin, referer, fetch, expected) in [
311 (
312 Some("https://api.example.com"),
313 None,
314 Some("same-origin"),
315 "called",
316 ),
317 (
318 Some("https://panel.example.com"),
319 None,
320 Some("same-site"),
321 "called",
322 ),
323 (
324 None,
325 Some("https://panel.example.com/settings"),
326 Some("same-site"),
327 "called",
328 ),
329 (
330 Some("https://evil.example.com"),
331 Some("https://api.example.com/"),
332 None,
333 "RequestOriginUntrusted",
334 ),
335 (
336 Some("https://api.example.com"),
337 Some("https://evil.example.com/"),
338 None,
339 "called",
340 ),
341 (
342 None,
343 Some("https://evil.example.com/"),
344 None,
345 "RequestRefererUntrusted",
346 ),
347 (Some("null"), None, None, "RequestOriginInvalid"),
348 (
349 None,
350 Some("missing-scheme/path"),
351 None,
352 "RequestSchemeInvalid",
353 ),
354 (None, None, Some("same-site"), "RequestSourceUntrusted"),
355 (
356 Some("https://api.example.com"),
357 None,
358 Some("cross-site"),
359 "RequestSourceUntrusted",
360 ),
361 (
362 Some("https://api.example.com"),
363 None,
364 Some("none"),
365 "RequestSourceUntrusted",
366 ),
367 (None, None, None, "RequestSourceMissing"),
368 ] {
369 let mut req = Request::builder()
370 .method("POST")
371 .header("cookie", "aster_csrf=t")
372 .header("x-csrf-token", "t");
373 if let Some(origin) = origin {
374 req = req.header("origin", origin);
375 }
376 if let Some(referer) = referer {
377 req = req.header("referer", referer);
378 }
379 if let Some(fetch) = fetch {
380 req = req.header("sec-fetch-site", fetch);
381 }
382 assert_eq!(
383 result(
384 app(CsrfTokenNames::default(), policy()),
385 req.body(Body::empty()).unwrap()
386 )
387 .await
388 .1,
389 expected
390 );
391 }
392 }
393
394 #[tokio::test]
395 async fn optional_mode_still_rejects_opaque_and_oversized_headers() {
396 let mut source = policy();
397 source.mode = RequestSourceMode::OptionalWhenPresent;
398 let router = app(CsrfTokenNames::default(), source);
399 for (name, value, expected) in [
400 (
401 "origin",
402 HeaderValue::from_bytes(&[0xff]).unwrap(),
403 "RequestOriginInvalid",
404 ),
405 (
406 "referer",
407 HeaderValue::from_bytes(&[0xff]).unwrap(),
408 "RequestSchemeInvalid",
409 ),
410 (
411 "sec-fetch-site",
412 HeaderValue::from_bytes(&[0xff]).unwrap(),
413 "RequestHeaderValueInvalid",
414 ),
415 (
416 "origin",
417 HeaderValue::from_str(&format!("https://{}.com", "a".repeat(2048))).unwrap(),
418 "RequestOriginInvalid",
419 ),
420 (
421 "referer",
422 HeaderValue::from_str(&format!("https://{}.com/", "a".repeat(529))).unwrap(),
423 "RequestRefererInvalid",
424 ),
425 (
426 "sec-fetch-site",
427 HeaderValue::from_str(&"a".repeat(65)).unwrap(),
428 "RequestHeaderValueInvalid",
429 ),
430 ] {
431 let req = Request::builder()
432 .method("POST")
433 .header(name, value)
434 .header("cookie", "aster_csrf=t")
435 .header("x-csrf-token", "t")
436 .body(Body::empty())
437 .unwrap();
438 assert_eq!(result(router.clone(), req).await.1, expected);
439 }
440 let req = Request::builder()
441 .method("POST")
442 .header("cookie", "aster_csrf=t")
443 .header("x-csrf-token", "t")
444 .body(Body::empty())
445 .unwrap();
446 assert_eq!(result(router, req).await.0, StatusCode::OK);
447 }
448
449 #[tokio::test]
450 async fn trusted_scheme_host_are_bounded_and_cannot_be_forged_by_headers() {
451 let mut source = policy();
452 source.scheme = "x".repeat(17);
453 let req = || {
454 Request::builder()
455 .method("POST")
456 .header("origin", "https://api.example.com")
457 .header("cookie", "aster_csrf=t")
458 .header("x-csrf-token", "t")
459 .body(Body::empty())
460 .unwrap()
461 };
462 assert_eq!(
463 result(app(CsrfTokenNames::default(), source), req())
464 .await
465 .1,
466 "RequestSchemeInvalid"
467 );
468 let mut source = policy();
469 source.host = "a".repeat(513);
470 assert_eq!(
471 result(app(CsrfTokenNames::default(), source), req())
472 .await
473 .1,
474 "RequestHostInvalid"
475 );
476 let req = Request::builder()
477 .method("POST")
478 .header("origin", "https://evil.example.com")
479 .header("host", "evil.example.com")
480 .header("x-forwarded-proto", "https")
481 .header("x-forwarded-host", "evil.example.com")
482 .header("cookie", "aster_csrf=t")
483 .header("x-csrf-token", "t")
484 .body(Body::empty())
485 .unwrap();
486 assert_eq!(
487 result(app(CsrfTokenNames::default(), policy()), req)
488 .await
489 .1,
490 "RequestOriginUntrusted"
491 );
492 }
493}