Skip to main content

actix_cloud/
csrf.rs

1//! CSRF protection based on the [double submit cookie] pattern.
2//!
3//! The token is stored in a cookie and must be echoed in a header (or a query
4//! parameter, generally only for websocket). Checks are applied per route through
5//! [`crate::build_router`], and run **after** the auth checker.
6//!
7//! [double submit cookie]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#alternative-using-a-double-submit-cookie-pattern
8use std::{future::Future, rc::Rc};
9
10use actix_web::{
11    dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
12    HttpMessage, HttpRequest,
13};
14use futures::future::{ready, LocalBoxFuture, Ready};
15use qstring::QString;
16
17use crate::router::CSRFType;
18
19/// CSRF middleware, configured through [`crate::build_router`].
20///
21/// `cookie` is the cookie holding the token, `header` is the header (or query
22/// parameter) expected to carry the same token, and `checker` is an extra validation
23/// callback invoked with the request and the token after both values match.
24pub struct Middleware<F> {
25    cookie: Rc<String>,
26    header: Rc<String>,
27    checker: Rc<F>,
28}
29
30impl<F> Clone for Middleware<F> {
31    fn clone(&self) -> Self {
32        Self {
33            cookie: self.cookie.clone(),
34            header: self.header.clone(),
35            checker: self.checker.clone(),
36        }
37    }
38}
39
40impl<F, Fut> Middleware<F>
41where
42    F: Fn(HttpRequest, String) -> Fut,
43    Fut: Future<Output = Result<bool, actix_web::Error>>,
44{
45    pub fn new(cookie: String, header: String, checker: F) -> Self {
46        Self {
47            cookie: Rc::new(cookie),
48            header: Rc::new(header),
49            checker: Rc::new(checker),
50        }
51    }
52}
53
54impl<S, B, F, Fut> Transform<S, ServiceRequest> for Middleware<F>
55where
56    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
57    S::Future: 'static,
58    B: 'static,
59    F: Fn(HttpRequest, String) -> Fut + 'static,
60    Fut: Future<Output = Result<bool, actix_web::Error>>,
61{
62    type Response = ServiceResponse<B>;
63    type Error = actix_web::Error;
64    type InitError = ();
65    type Transform = MiddlewareService<S, F>;
66    type Future = Ready<Result<Self::Transform, Self::InitError>>;
67
68    fn new_transform(&self, service: S) -> Self::Future {
69        ready(Ok(MiddlewareService {
70            service: Rc::new(service),
71            cookie: self.cookie.clone(),
72            header: self.header.clone(),
73            checker: self.checker.clone(),
74        }))
75    }
76}
77
78pub struct MiddlewareService<S, F> {
79    service: Rc<S>,
80    cookie: Rc<String>,
81    header: Rc<String>,
82    checker: Rc<F>,
83}
84
85impl<S, B, F, Fut> MiddlewareService<S, F>
86where
87    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
88    S::Future: 'static,
89    B: 'static,
90    F: Fn(HttpRequest, String) -> Fut + 'static,
91    Fut: Future<Output = Result<bool, actix_web::Error>>,
92{
93    fn get_safe_header(req: &ServiceRequest, name: &str) -> Option<String> {
94        let mut values = req
95            .headers()
96            .get_all(name)
97            .map(|x| x.to_str().unwrap_or_default())
98            .filter(|x| !x.is_empty());
99        let ret = values.next()?;
100        // Reject duplicated header values to avoid smuggling.
101        if values.next().is_some() {
102            return None;
103        }
104        Some(ret.to_owned())
105    }
106
107    async fn check_csrf(
108        req: &ServiceRequest,
109        cookie: &str,
110        header: &str,
111        checker: Rc<F>,
112        allow_param: bool,
113    ) -> Result<bool, actix_web::Error> {
114        let Some(cookie) = req.cookie(cookie) else {
115            return Ok(false);
116        };
117        let mut csrf = Self::get_safe_header(req, header);
118        if csrf.is_none() && allow_param {
119            let qs = QString::from(req.query_string());
120            csrf = qs.get(header).map(ToOwned::to_owned);
121        }
122        let Some(csrf) = csrf else {
123            return Ok(false);
124        };
125        if csrf != cookie.value() {
126            return Ok(false);
127        }
128        checker(req.request().clone(), csrf).await
129    }
130}
131
132impl<S, B, F, Fut> Service<ServiceRequest> for MiddlewareService<S, F>
133where
134    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
135    S::Future: 'static,
136    B: 'static,
137    F: Fn(HttpRequest, String) -> Fut + 'static,
138    Fut: Future<Output = Result<bool, actix_web::Error>>,
139{
140    type Response = ServiceResponse<B>;
141    type Error = actix_web::Error;
142    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
143
144    forward_ready!(service);
145
146    fn call(&self, req: ServiceRequest) -> Self::Future {
147        let srv = self.service.clone();
148        let header = self.header.clone();
149        let cookie = self.cookie.clone();
150        let checker = self.checker.clone();
151        Box::pin(async move {
152            // `CSRFType` is inserted per-route by the router guard; the middleware must
153            // be used through `build_router`.
154            let csrf = req.extensions().get::<CSRFType>().unwrap().to_owned();
155            let enforce = !csrf.is_disabled()
156                && (csrf.is_force_header() || csrf.is_force_param() || !req.method().is_safe());
157            if enforce {
158                // `Param`/`ForceParam` also accept the token as a query parameter,
159                // generally only used for websocket.
160                let allow_param = csrf.is_param() || csrf.is_force_param();
161                if !Self::check_csrf(&req, &cookie, &header, checker, allow_param).await? {
162                    return Err(actix_web::error::ErrorBadRequest("CSRF check failed"));
163                }
164            }
165            srv.call(req).await
166        })
167    }
168}