actix_csrf_middleware/
lib.rs

1use actix_http::{header::HeaderMap, StatusCode};
2#[cfg(feature = "actix-session")]
3use actix_session::SessionExt;
4use actix_utils::future::Either;
5use actix_web::{
6    body::{EitherBody, MessageBody},
7    cookie::{time, Cookie, SameSite},
8    dev::forward_ready,
9    dev::{Service, ServiceRequest, ServiceResponse, Transform},
10    http::{header, Method},
11    web::BytesMut,
12    Error, FromRequest, HttpMessage, HttpRequest, HttpResponse, HttpResponseBuilder,
13};
14use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
15use futures_util::{
16    future::{err, ok, Ready},
17    ready,
18    stream::StreamExt,
19};
20use hmac::{Hmac, Mac};
21use log::{error, warn};
22use pin_project_lite::pin_project;
23use rand::RngCore;
24use sha2::Sha256;
25use std::{
26    collections::HashMap,
27    future::Future,
28    marker::PhantomData,
29    pin::Pin,
30    rc::Rc,
31    task::{Context, Poll},
32};
33use subtle::ConstantTimeEq;
34
35// Strict security defaults for pre-session cookie
36const PRE_SESSION_HTTP_ONLY: bool = true;
37const PRE_SESSION_SECURE: bool = true;
38const PRE_SESSION_SAME_SITE: SameSite = SameSite::Strict;
39
40/// Cookie name or actix-session key used to store the authorized (session-bound) token
41pub const DEFAULT_CSRF_TOKEN_KEY: &str = "CSRF";
42
43/// Cookie name used to store the anonymous (pre-session) token
44pub const DEFAULT_CSRF_ANON_TOKEN_KEY: &str = "CSRF-ANON";
45
46/// Csrf token field name in `application/x-www-form-urlencoded` or `application/json` body.
47/// `CsrfMiddleware` will try to extract a token from that field.
48pub const DEFAULT_CSRF_TOKEN_FIELD: &str = "csrf_token";
49pub const DEFAULT_CSRF_TOKEN_HEADER: &str = "X-CSRF-Token";
50
51/// Key of user session created outside the middleware. It's cookie name or actix-session key
52/// depending on enabled `session` feature and middleware core will extract value by this key
53/// to use this unique session id in HMAC hashes. That's how `CsrfMiddleware` can be integrated
54/// into existing applications that already have authorization logic.
55pub const DEFAULT_SESSION_ID_KEY: &str = "id";
56
57/// Cookie name of pre-session generated by `CsrfMiddleware` for allowed unauthorized routes
58/// that need to mutate data when there's not authorized user yet. For example
59/// in cases such as registration, login or newsletter subscription.
60pub const CSRF_PRE_SESSION_KEY: &str = "pre-session";
61
62/// Standard 256-bit encryption
63const TOKEN_LEN: usize = 32; // 32 bytes -> 256 bits
64
65type HmacSha256 = Hmac<Sha256>;
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub enum TokenClass {
69    Anonymous,
70    Authorized,
71}
72
73impl TokenClass {
74    fn as_str(&self) -> &'static str {
75        match self {
76            TokenClass::Anonymous => "anon",
77            TokenClass::Authorized => "auth",
78        }
79    }
80}
81
82/// `CsrfPattern` allows use to configure `CsrfMiddleware` to read and store csrf tokens and
83/// user sessions in client's browser cookie or in any persistent storage like Redis,
84/// Postgres or in-memory that implements `actix_session::storage::SessionStore` trait if
85/// you have enabled `session` feature.
86#[derive(Clone, PartialEq)]
87pub enum CsrfPattern {
88    #[cfg(feature = "actix-session")]
89    SynchronizerToken,
90    DoubleSubmitCookie,
91}
92
93#[derive(Clone)]
94pub struct CsrfDoubleSubmitCookie {
95    pub http_only: bool,
96    pub secure: bool,
97    pub same_site: SameSite,
98}
99
100#[derive(Clone)]
101pub struct CsrfMiddlewareConfig {
102    pub pattern: CsrfPattern,
103    pub manual_multipart: bool,
104    pub session_id_cookie_name: String,
105    /// Authorized (session-bound) tokens
106    pub token_cookie_name: String,
107    /// Anonymous (pre-session) tokens
108    pub anon_token_cookie_name: String,
109    #[cfg(feature = "actix-session")]
110    /// Anonymous (pre-session) token key for SynchronizerToken    
111    pub anon_session_key_name: String,
112    pub token_form_field: String,
113    pub token_header_name: String,
114    pub token_cookie_config: Option<CsrfDoubleSubmitCookie>,
115    pub secret_key: Vec<u8>,
116    pub skip_for: Vec<String>,
117    /// Enforce Origin/Referer checks for mutating requests
118    pub enforce_origin: bool,
119    /// Allowed origins (scheme://host[:port]) when enforce_origin = true
120    pub allowed_origins: Vec<String>,
121    /// Maximum allowed body bytes to read when extracting
122    /// CSRF tokens from body (POST/PUT/PATCH/DELETE)
123    pub max_body_bytes: usize,
124}
125
126impl CsrfMiddlewareConfig {
127    #[cfg(feature = "actix-session")]
128    pub fn synchronizer_token(secret_key: &[u8]) -> Self {
129        check_secret_key(secret_key);
130
131        CsrfMiddlewareConfig {
132            pattern: CsrfPattern::SynchronizerToken,
133            session_id_cookie_name: DEFAULT_SESSION_ID_KEY.to_string(),
134            token_cookie_name: DEFAULT_CSRF_TOKEN_KEY.into(),
135            anon_token_cookie_name: DEFAULT_CSRF_ANON_TOKEN_KEY.into(),
136            #[cfg(feature = "actix-session")]
137            anon_session_key_name: format!("{}-anon", DEFAULT_CSRF_TOKEN_KEY),
138            token_form_field: DEFAULT_CSRF_TOKEN_FIELD.into(),
139            token_header_name: DEFAULT_CSRF_TOKEN_HEADER.into(),
140            token_cookie_config: None,
141            secret_key: secret_key.into(),
142            skip_for: vec![],
143            manual_multipart: false,
144            enforce_origin: false,
145            allowed_origins: vec![],
146            max_body_bytes: 2 * 1024 * 1024, // 2 MiB default
147        }
148    }
149
150    pub fn double_submit_cookie(secret_key: &[u8]) -> Self {
151        check_secret_key(secret_key);
152
153        CsrfMiddlewareConfig {
154            pattern: CsrfPattern::DoubleSubmitCookie,
155            session_id_cookie_name: DEFAULT_SESSION_ID_KEY.to_string(),
156            token_cookie_name: DEFAULT_CSRF_TOKEN_KEY.into(),
157            anon_token_cookie_name: DEFAULT_CSRF_ANON_TOKEN_KEY.into(),
158            #[cfg(feature = "actix-session")]
159            anon_session_key_name: format!("{}-anon", DEFAULT_CSRF_TOKEN_KEY),
160            token_form_field: DEFAULT_CSRF_TOKEN_FIELD.into(),
161            token_header_name: DEFAULT_CSRF_TOKEN_HEADER.into(),
162            token_cookie_config: Some(CsrfDoubleSubmitCookie {
163                http_only: false, // Should be false for double-submit cookie
164                secure: true,
165                same_site: SameSite::Strict,
166            }),
167            secret_key: secret_key.into(),
168            skip_for: vec![],
169            manual_multipart: false,
170            enforce_origin: false,
171            allowed_origins: vec![],
172            max_body_bytes: 2 * 1024 * 1024,
173        }
174    }
175
176    pub fn with_multipart(mut self, multipart: bool) -> Self {
177        self.manual_multipart = multipart;
178        self
179    }
180
181    pub fn with_max_body_bytes(mut self, limit: usize) -> Self {
182        self.max_body_bytes = limit;
183        self
184    }
185
186    pub fn with_token_cookie_config(mut self, config: CsrfDoubleSubmitCookie) -> Self {
187        self.token_cookie_config = Some(config);
188        self
189    }
190
191    pub fn with_skip_for(mut self, patches: Vec<String>) -> Self {
192        self.skip_for = patches;
193        self
194    }
195
196    pub fn with_enforce_origin(mut self, enforce: bool, allowed: Vec<String>) -> Self {
197        self.enforce_origin = enforce;
198        self.allowed_origins = allowed;
199        self
200    }
201}
202
203pub struct CsrfMiddleware {
204    config: Rc<CsrfMiddlewareConfig>,
205}
206
207impl CsrfMiddleware {
208    pub fn new(config: CsrfMiddlewareConfig) -> Self {
209        Self {
210            config: Rc::new(config),
211        }
212    }
213}
214
215impl<S, B> Transform<S, ServiceRequest> for CsrfMiddleware
216where
217    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
218    B: MessageBody,
219{
220    type Response = ServiceResponse<EitherBody<B>>;
221    type Error = Error;
222    type Transform = CsrfMiddlewareService<S>;
223    type InitError = ();
224    type Future = Ready<Result<Self::Transform, Self::InitError>>;
225
226    fn new_transform(&self, service: S) -> Self::Future {
227        ok(CsrfMiddlewareService {
228            service: Rc::new(service),
229            config: self.config.clone(),
230        })
231    }
232}
233
234pub struct CsrfMiddlewareService<S> {
235    service: Rc<S>,
236    config: Rc<CsrfMiddlewareConfig>,
237}
238
239impl<S> CsrfMiddlewareService<S> {
240    fn get_session_from_cookie(&self, req: &ServiceRequest) -> (String, bool, TokenClass) {
241        // Try to extract from session id cookie first,
242        // if nothing found then check pre-session or create new one.
243        if let Some(id) = req
244            .cookie(&self.config.session_id_cookie_name)
245            .map(|c| c.value().to_string())
246        {
247            (id, false, TokenClass::Authorized)
248        } else if let Some(val) = req
249            .cookie(CSRF_PRE_SESSION_KEY)
250            .map(|c| c.value().to_string())
251        {
252            // Validate signed/encrypted pre-session value; if invalid, rotate
253            if let Some(pre_id) = decode_pre_session_cookie(&val, self.config.secret_key.as_ref()) {
254                (pre_id, false, TokenClass::Anonymous)
255            } else {
256                (generate_random_token(), true, TokenClass::Anonymous)
257            }
258        } else {
259            // Generate pre-session id here
260            (generate_random_token(), true, TokenClass::Anonymous)
261        }
262    }
263
264    fn get_true_token(
265        &self,
266        req: &ServiceRequest,
267        session_id: Option<&str>,
268        class: TokenClass,
269    ) -> (String, bool) {
270        match self.config.pattern {
271            // If corresponding feature enabled then get token from persistent session storage
272            #[cfg(feature = "actix-session")]
273            CsrfPattern::SynchronizerToken => {
274                let session = req.get_session();
275                let key = match class {
276                    TokenClass::Authorized => &self.config.token_cookie_name,
277                    TokenClass::Anonymous => &self.config.anon_session_key_name,
278                };
279
280                let found = session.get::<String>(key).ok().flatten();
281
282                match found {
283                    Some(tok) => (tok, false),
284                    None => (generate_random_token(), true),
285                }
286            }
287            // Check for csrf token in request cookies
288            CsrfPattern::DoubleSubmitCookie => {
289                let (cookie_name, ctx) = match class {
290                    TokenClass::Authorized => {
291                        (&self.config.token_cookie_name, TokenClass::Authorized)
292                    }
293                    TokenClass::Anonymous => {
294                        (&self.config.anon_token_cookie_name, TokenClass::Anonymous)
295                    }
296                };
297
298                let token = { req.cookie(cookie_name).map(|c| c.value().to_string()) };
299
300                match token {
301                    Some(tok) => (tok, false),
302                    None => {
303                        let secret = self.config.secret_key.as_ref();
304                        let tok = generate_hmac_token_ctx(
305                            ctx,
306                            session_id.expect("Session or pre-session id is passed"),
307                            secret,
308                        );
309                        (tok, true)
310                    }
311                }
312            }
313        }
314    }
315
316    fn should_skip_validation(&self, req: &ServiceRequest) -> bool {
317        let req_path = req.path();
318        self.config
319            .skip_for
320            .iter()
321            .any(|prefix| req_path.starts_with(prefix))
322    }
323}
324
325impl<S, B> Service<ServiceRequest> for CsrfMiddlewareService<S>
326where
327    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
328    B: MessageBody,
329{
330    type Response = ServiceResponse<EitherBody<B>>;
331    type Error = Error;
332    type Future = Either<CsrfTokenValidator<S, B>, Ready<Result<Self::Response, Self::Error>>>;
333
334    forward_ready!(service);
335
336    fn call(&self, req: ServiceRequest) -> Self::Future {
337        if self.should_skip_validation(&req) {
338            let resp = CsrfResponse {
339                fut: self.service.call(req),
340                config: None,
341                set_token: None,
342                set_pre_session: None,
343                token_class: None,
344                remove_anon_session: false,
345                _phantom: PhantomData,
346            };
347            return Either::left(CsrfTokenValidator::CsrfResponse { response: resp });
348        }
349
350        // Get current token from cookie or actix-session or generate new one
351        let (true_token, should_set_token, cookie_session, token_class): (
352            String,
353            bool,
354            Option<(String, bool)>,
355            Option<TokenClass>,
356        ) = match self.config.pattern {
357            CsrfPattern::DoubleSubmitCookie => {
358                let (session_id, set_pre_session, token_class) = self.get_session_from_cookie(&req);
359                let (true_token, should_set_token) =
360                    self.get_true_token(&req, Some(&session_id), token_class);
361                (
362                    true_token,
363                    should_set_token,
364                    Some((session_id, set_pre_session)),
365                    Some(token_class),
366                )
367            }
368            #[cfg(feature = "actix-session")]
369            CsrfPattern::SynchronizerToken => {
370                // Derive class from cookies and set pre-session cookie if needed
371                let (session_id, set_pre_session, token_class) = self.get_session_from_cookie(&req);
372                let (token, should_set_token) = self.get_true_token(&req, None, token_class);
373
374                (
375                    token,
376                    should_set_token,
377                    Some((session_id, set_pre_session)),
378                    Some(token_class),
379                )
380            }
381        };
382
383        req.extensions_mut().insert(CsrfToken(true_token.clone()));
384        req.extensions_mut().insert(self.config.clone());
385
386        let is_mutating = matches!(
387            *req.method(),
388            Method::POST | Method::PUT | Method::PATCH | Method::DELETE
389        );
390
391        // Skip validation for read only requests, but csrf token still should be
392        // added to the response when should_set_token flag is set to true.
393        if !is_mutating {
394            let mut set_token_bytes = if should_set_token {
395                Some(true_token.clone().into_bytes())
396            } else {
397                None
398            };
399
400            let session_id = if let Some((ref session_id, set_pre_session)) = cookie_session {
401                if set_pre_session {
402                    Some(session_id.clone().into_bytes())
403                } else {
404                    None
405                }
406            } else {
407                None
408            };
409
410            // Ensure an authorized token cookie exists after login (DoubleSubmitCookie only)
411            if self.config.pattern == CsrfPattern::DoubleSubmitCookie {
412                if let (Some(TokenClass::Authorized), Some((ref sess_id, _))) =
413                    (token_class, cookie_session.as_ref())
414                {
415                    // If no authorized token cookie yet, issue one now
416                    if req.cookie(&self.config.token_cookie_name).is_none() {
417                        let tok = generate_hmac_token_ctx(
418                            TokenClass::Authorized,
419                            sess_id,
420                            self.config.secret_key.as_ref(),
421                        );
422                        set_token_bytes = Some(tok.into_bytes());
423                    }
424                }
425            }
426
427            let remove_anon_session = matches!(token_class, Some(TokenClass::Authorized));
428
429            let resp = CsrfResponse {
430                fut: self.service.call(req),
431                config: Some(self.config.clone()),
432                set_token: set_token_bytes,
433                set_pre_session: session_id,
434                token_class,
435                remove_anon_session,
436                _phantom: PhantomData,
437            };
438
439            return Either::left(CsrfTokenValidator::CsrfResponse { response: resp });
440        }
441
442        // Optionally enforce Origin/Referer before token checks
443        if self.config.enforce_origin && !origin_allowed(req.headers(), &self.config) {
444            let resp = HttpResponse::with_body(StatusCode::FORBIDDEN, "Invalid request origin");
445            return Either::right(ok(req
446                .into_response(resp)
447                .map_into_boxed_body()
448                .map_into_right_body()));
449        }
450
451        // Otherwise, process mutating request with token
452        // extraction from the body and future validation.
453
454        // Handle multipart form data requests
455        if let Some(ct) = req
456            .headers()
457            .get(header::CONTENT_TYPE)
458            .and_then(|hv| hv.to_str().ok())
459        {
460            if ct.starts_with("multipart/form-data") {
461                // Deny any multipart/form-data requests if
462                // it isn't allowed explicitly by the consumer.
463                if !self.config.manual_multipart {
464                    let resp = HttpResponse::with_body(
465                        StatusCode::BAD_REQUEST,
466                        "Multipart form data is not enabled by csrf config",
467                    );
468
469                    return Either::right(ok(req
470                        .into_response(resp)
471                        .map_into_boxed_body()
472                        .map_into_right_body()));
473                }
474
475                // Then consumer reads body, extracts and
476                // verifies csrf tokens manually in their handlers.
477                let resp = CsrfResponse {
478                    fut: self.service.call(req),
479                    config: Some(self.config.clone()),
480                    set_token: None,
481                    set_pre_session: None,
482                    token_class: None,
483                    remove_anon_session: false,
484                    _phantom: PhantomData,
485                };
486
487                return Either::left(CsrfTokenValidator::CsrfResponse { response: resp });
488            }
489        }
490
491        let (session_id, token_class) = if let Some((session_id, _)) = cookie_session {
492            (Some(session_id), token_class)
493        } else {
494            (None, token_class)
495        };
496
497        // Try to extract csrf token from header
498        let header_token = req
499            .headers()
500            .get(&self.config.token_header_name)
501            .and_then(|hv| hv.to_str().ok())
502            .map(|s| s.to_string());
503
504        // Fastest and easiest way when token just received in headers
505        if let Some(token) = header_token {
506            return Either::left(CsrfTokenValidator::MutatingRequest {
507                service: self.service.clone(),
508                config: self.config.clone(),
509                true_token: true_token.into_bytes(), // TODO Find a way avoid such allocations
510                client_token: token.into_bytes(),
511                session_id,
512                token_class,
513                req: Some(req),
514            });
515        }
516
517        // For mutating requests without header token, read body first
518        Either::left(CsrfTokenValidator::ReadingBody {
519            req: Some(req),
520            config: self.config.clone(),
521            service: self.service.clone(),
522            true_token: true_token.into_bytes(),
523            session_id,
524            token_class,
525        })
526    }
527}
528
529pin_project! {
530    #[project = CsrfTokenValidatorProj]
531    pub enum CsrfTokenValidator<S, B>
532    where
533        S: Service<ServiceRequest>,
534        B: MessageBody,
535    {
536        CsrfResponse {
537            #[pin]
538            response: CsrfResponse<S, B>,
539        },
540        MutatingRequest {
541            service: Rc<S>,
542            config: Rc<CsrfMiddlewareConfig>,
543            true_token: Vec<u8>,
544            client_token: Vec<u8>,
545            session_id: Option<String>,
546            token_class: Option<TokenClass>,
547            req: Option<ServiceRequest>
548        },
549        ReadingBody {
550            service: Rc<S>,
551            config: Rc<CsrfMiddlewareConfig>,
552            req: Option<ServiceRequest>,
553            true_token: Vec<u8>,
554            session_id: Option<String>,
555            token_class: Option<TokenClass>,
556        },
557    }
558}
559
560impl<S, B> Future for CsrfTokenValidator<S, B>
561where
562    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
563    B: MessageBody,
564{
565    type Output = Result<ServiceResponse<EitherBody<B>>, Error>;
566
567    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
568        match self.as_mut().project() {
569            CsrfTokenValidatorProj::CsrfResponse { response } => response.poll(cx),
570            CsrfTokenValidatorProj::MutatingRequest {
571                service,
572                config,
573                true_token,
574                client_token,
575                session_id,
576                token_class,
577                req,
578            } => {
579                if let Some(req) = req.take() {
580                    // Session id cannot be empty with DoubleSubmitCookie pattern
581                    let session_id = if config.pattern == CsrfPattern::DoubleSubmitCookie {
582                        if let Some(id) = session_id.take() {
583                            Some(id)
584                        } else {
585                            let resp = HttpResponse::with_body(
586                                StatusCode::INTERNAL_SERVER_ERROR,
587                                "Session id is empty in csrf token validator".to_string(),
588                            );
589
590                            return Poll::Ready(Ok(req
591                                .into_response(resp)
592                                .map_into_boxed_body()
593                                .map_into_right_body()));
594                        }
595                    } else {
596                        None
597                    };
598
599                    // Validate client token based on the pattern
600                    let valid = match &config.pattern {
601                        #[cfg(feature = "actix-session")]
602                        CsrfPattern::SynchronizerToken => {
603                            if eq_tokens(true_token, client_token) {
604                                true
605                            } else {
606                                let alt_valid = {
607                                    let session = req.get_session();
608                                    let alt_key = match token_class
609                                        .as_ref()
610                                        .copied()
611                                        .unwrap_or(TokenClass::Authorized)
612                                    {
613                                        TokenClass::Authorized => &config.anon_session_key_name,
614                                        TokenClass::Anonymous => &config.token_cookie_name,
615                                    };
616                                    let alt = session.get::<String>(alt_key).ok().flatten();
617
618                                    alt.map(|t| eq_tokens(t.as_bytes(), client_token))
619                                        .unwrap_or(false)
620                                };
621
622                                alt_valid
623                            }
624                        }
625                        CsrfPattern::DoubleSubmitCookie => {
626                            let ctx = token_class
627                                .as_ref()
628                                .copied()
629                                .unwrap_or(TokenClass::Anonymous);
630                            validate_hmac_token_ctx(
631                                ctx,
632                                session_id
633                                    .as_deref()
634                                    .expect("session id cannot be empty is hmac validation"),
635                                client_token,
636                                config.secret_key.as_ref(),
637                            )
638                            .unwrap_or(false)
639                        }
640                    };
641
642                    if !valid {
643                        let resp = HttpResponse::BadRequest().body("Invalid CSRF token");
644                        return Poll::Ready(Ok(req
645                            .into_response(resp)
646                            .map_into_boxed_body()
647                            .map_into_right_body()));
648                    }
649
650                    // Rotate token based on configured pattern after every successful validation
651                    let new_token = match &config.pattern {
652                        #[cfg(feature = "actix-session")]
653                        CsrfPattern::SynchronizerToken => generate_random_token(),
654                        CsrfPattern::DoubleSubmitCookie => {
655                            let ctx = token_class
656                                .as_ref()
657                                .copied()
658                                .unwrap_or(TokenClass::Anonymous);
659                            generate_hmac_token_ctx(
660                                ctx,
661                                session_id
662                                    .as_deref()
663                                    .expect("session id cannot be empty is hmac validation"),
664                                config.secret_key.as_ref(),
665                            )
666                        }
667                    };
668
669                    let resp = CsrfResponse {
670                        fut: service.call(req),
671                        config: Some(config.clone()),
672                        set_token: Some(new_token.into_bytes()),
673                        set_pre_session: None,
674                        token_class: *token_class,
675                        remove_anon_session: false,
676                        _phantom: PhantomData,
677                    };
678
679                    self.set(CsrfTokenValidator::CsrfResponse { response: resp });
680
681                    cx.waker().wake_by_ref(); // wake for the next pool
682                    Poll::Pending
683                } else {
684                    error!("request already taken in csrf validator's state machine");
685
686                    Poll::Ready(Err(actix_web::error::ErrorInternalServerError(
687                        "Request was already taken",
688                    )))
689                }
690            }
691            CsrfTokenValidatorProj::ReadingBody {
692                service,
693                config,
694                req,
695                true_token,
696                session_id,
697                token_class,
698            } => {
699                if let Some(mut request) = req.take() {
700                    let mut body_bytes = BytesMut::new();
701                    let mut payload = request.take_payload();
702
703                    while let Some(chunk_result) = ready!(payload.poll_next_unpin(cx)) {
704                        match chunk_result {
705                            Ok(bytes) => {
706                                body_bytes.extend_from_slice(&bytes);
707
708                                if body_bytes.len() > config.max_body_bytes {
709                                    let resp = HttpResponse::with_body(
710                                        StatusCode::PAYLOAD_TOO_LARGE,
711                                        "Request body too large for CSRF token extraction",
712                                    );
713
714                                    return Poll::Ready(Ok(request
715                                        .into_response(resp)
716                                        .map_into_boxed_body()
717                                        .map_into_right_body()));
718                                }
719                            }
720                            Err(e) => {
721                                return Poll::Ready(Err(actix_web::error::ErrorBadRequest(e)));
722                            }
723                        }
724                    }
725
726                    // Try to extract token from body
727                    let client_token = match sync_read_token_from_body(
728                        request.headers(),
729                        &body_bytes,
730                        &config.token_form_field,
731                    ) {
732                        Some(token) => token.into_bytes(),
733                        None => {
734                            let res = HttpResponse::BadRequest().body("CSRF token is required");
735                            return Poll::Ready(Ok(request
736                                .into_response(res)
737                                .map_into_boxed_body()
738                                .map_into_right_body()));
739                        }
740                    };
741
742                    // Restore the body for the next handler
743                    request.set_payload(actix_web::dev::Payload::from(body_bytes.freeze()));
744
745                    let next_state = {
746                        let service = service.clone();
747                        let config = config.clone();
748                        let true_token = std::mem::take(true_token);
749                        let session_id = session_id.take();
750                        let token_class = token_class.take();
751                        let req = Some(request);
752
753                        CsrfTokenValidator::MutatingRequest {
754                            service,
755                            config,
756                            true_token,
757                            client_token,
758                            session_id,
759                            token_class,
760                            req,
761                        }
762                    };
763
764                    // Drop borrows from projection before mutating self
765                    self.set(next_state);
766
767                    cx.waker().wake_by_ref(); // wake for the next pool
768                    Poll::Pending
769                } else {
770                    error!("request already taken in csrf validator's state machine");
771
772                    Poll::Ready(Err(actix_web::error::ErrorInternalServerError(
773                        "Request was already taken",
774                    )))
775                }
776            }
777        }
778    }
779}
780
781fn sync_read_token_from_body(
782    headers: &HeaderMap,
783    body: &[u8],
784    token_field: &str,
785) -> Option<String> {
786    if let Some(ct) = headers.get(header::CONTENT_TYPE) {
787        if let Ok(ct) = ct.to_str() {
788            if ct.starts_with("application/json") {
789                if let Ok(json) = serde_json::from_slice::<serde_json::Value>(body) {
790                    return json
791                        .get(token_field)
792                        .and_then(|v| v.as_str().map(String::from));
793                }
794            } else if ct.starts_with("application/x-www-form-urlencoded") {
795                if let Ok(form) = serde_urlencoded::from_bytes::<HashMap<String, String>>(body) {
796                    return form.get(token_field).cloned();
797                }
798            } else {
799                warn!("unsupported request content type, unable to extract and verify csrf token");
800            }
801        }
802    }
803    None
804}
805
806pin_project! {
807    pub struct CsrfResponse<S, B>
808    where
809        S: Service<ServiceRequest>,
810        B: MessageBody,
811    {
812        #[pin]
813        fut: S::Future,
814        config: Option<Rc<CsrfMiddlewareConfig>>,
815        set_token: Option<Vec<u8>>,
816        set_pre_session: Option<Vec<u8>>,
817        token_class: Option<TokenClass>,
818        remove_anon_session: bool,
819        _phantom: PhantomData<B>,
820    }
821}
822
823impl<S, B> Future for CsrfResponse<S, B>
824where
825    B: MessageBody,
826    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
827{
828    type Output = Result<ServiceResponse<EitherBody<B>>, Error>;
829
830    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
831        let this = self.as_mut().project();
832        match ready!(this.fut.poll(cx)) {
833            Ok(mut resp) => {
834                let config = match &this.config {
835                    Some(config) => config,
836                    None => {
837                        let res = HttpResponse::InternalServerError()
838                            .body("An empty csrf middleware config passed into csrf response");
839
840                        error!("unable to extract csrf middleware config in csrf response");
841                        return Poll::Ready(Ok(resp
842                            .into_response(res)
843                            .map_into_boxed_body()
844                            .map_into_right_body()));
845                    }
846                };
847
848                // Set pre-session if requested
849                if let Some(pre_session_bytes) = this.set_pre_session {
850                    let pre_session_id = std::str::from_utf8(pre_session_bytes)?;
851                    let cookie_val =
852                        encode_pre_session_cookie(pre_session_id, config.secret_key.as_ref());
853
854                    match resp.response_mut().add_cookie(
855                        &Cookie::build(CSRF_PRE_SESSION_KEY, cookie_val)
856                            .http_only(PRE_SESSION_HTTP_ONLY)
857                            .secure(PRE_SESSION_SECURE)
858                            .same_site(PRE_SESSION_SAME_SITE)
859                            .path("/")
860                            .finish(),
861                    ) {
862                        Ok(_) => {}
863                        Err(e) => {
864                            let res = HttpResponse::InternalServerError()
865                                .body("Unable to set pre-session cookie");
866
867                            error!("unable to set pre-session cookie in csrf response: {:?}", e);
868                            return Poll::Ready(Ok(resp
869                                .into_response(res)
870                                .map_into_boxed_body()
871                                .map_into_right_body()));
872                        }
873                    }
874                }
875
876                // If requested, clear pre-session cookie and anon token cookie
877                if *this.remove_anon_session {
878                    // Expire pre-session
879                    let mut del = Cookie::new(CSRF_PRE_SESSION_KEY.to_string(), "");
880                    del.set_max_age(time::Duration::seconds(0));
881                    del.set_expires(time::OffsetDateTime::UNIX_EPOCH);
882                    del.set_path("/");
883                    del.set_http_only(PRE_SESSION_HTTP_ONLY);
884                    del.set_secure(PRE_SESSION_SECURE);
885                    del.set_same_site(PRE_SESSION_SAME_SITE);
886
887                    if let Err(e) = resp.response_mut().add_cookie(&del) {
888                        let res = HttpResponse::InternalServerError()
889                            .body("Failed to expire pre-session cookie");
890
891                        error!(
892                            "unable to expire pre-session cookie in csrf response: {:?}",
893                            e
894                        );
895                        return Poll::Ready(Ok(resp
896                            .into_response(res)
897                            .map_into_boxed_body()
898                            .map_into_right_body()));
899                    }
900
901                    // Expire anonymous token cookie
902                    if matches!(config.pattern, CsrfPattern::DoubleSubmitCookie) {
903                        let mut del_tok = Cookie::new(config.anon_token_cookie_name.clone(), "");
904                        del_tok.set_max_age(time::Duration::seconds(0));
905                        del_tok.set_expires(time::OffsetDateTime::UNIX_EPOCH);
906                        del_tok.set_path("/");
907
908                        if let Err(e) = resp.response_mut().add_cookie(&del_tok) {
909                            let res = HttpResponse::InternalServerError()
910                                .body("Failed to expire anon token cookie");
911
912                            error!(
913                                "unable to expire anon token cookie in csrf response: {:?}",
914                                e
915                            );
916                            return Poll::Ready(Ok(resp
917                                .into_response(res)
918                                .map_into_boxed_body()
919                                .map_into_right_body()));
920                        }
921                    }
922                }
923
924                // Based on configured pattern, set a new token or rotate
925                // the old one for the service response if pattern is passed.
926                if let Some(token_bytes) = this.set_token.take() {
927                    let new_token = match String::from_utf8(token_bytes) {
928                        Ok(token) => token,
929                        Err(e) => {
930                            let res = HttpResponse::with_body(
931                                StatusCode::INTERNAL_SERVER_ERROR,
932                                "Failed to convert bytes into string in csrf response",
933                            );
934
935                            error!(
936                                "unable to convert token bytes into string in csrf response: {:?}",
937                                e
938                            );
939
940                            return Poll::Ready(Ok(resp
941                                .into_response(res)
942                                .map_into_boxed_body()
943                                .map_into_right_body()));
944                        }
945                    };
946
947                    match config.pattern {
948                        #[cfg(feature = "actix-session")]
949                        CsrfPattern::SynchronizerToken => {
950                            // Remove anon session key if requested (best-effort)
951                            if *this.remove_anon_session {
952                                let _ = resp
953                                    .request()
954                                    .get_session()
955                                    .remove(&config.anon_session_key_name);
956                            }
957
958                            // Set a new token into actix session under key decided by class
959                            let key = match this.token_class.unwrap_or(TokenClass::Authorized) {
960                                TokenClass::Authorized => &config.token_cookie_name,
961                                TokenClass::Anonymous => &config.anon_session_key_name,
962                            };
963
964                            match resp.request().get_session().insert(key, new_token) {
965                                Ok(()) => {}
966                                Err(e) => {
967                                    let res = HttpResponse::with_body(
968                                        StatusCode::INTERNAL_SERVER_ERROR,
969                                        "Failed to insert CSRF token into session",
970                                    );
971
972                                    error!("unable to set a csrf token with actix session in csrf response: {:?}",e);
973                                    return Poll::Ready(Ok(resp
974                                        .into_response(res)
975                                        .map_into_boxed_body()
976                                        .map_into_right_body()));
977                                }
978                            }
979                        }
980                        CsrfPattern::DoubleSubmitCookie => {
981                            let cookie_config = match &config.token_cookie_config {
982                                Some(config) => config,
983                                None => {
984                                    let res = HttpResponse::InternalServerError().body(
985                                        "An empty csrf cookie config passed into csrf response",
986                                    );
987
988                                    error!(
989                                        "unable to extract token_cookie_config in csrf response"
990                                    );
991                                    return Poll::Ready(Ok(resp
992                                        .into_response(res)
993                                        .map_into_boxed_body()
994                                        .map_into_right_body()));
995                                }
996                            };
997
998                            // Choose cookie name based on token class
999                            let cookie_name =
1000                                match this.token_class.unwrap_or(TokenClass::Anonymous) {
1001                                    TokenClass::Authorized => &config.token_cookie_name,
1002                                    TokenClass::Anonymous => &config.anon_token_cookie_name,
1003                                };
1004
1005                            let new_token_cookie = Cookie::build(cookie_name, new_token)
1006                                .http_only(cookie_config.http_only)
1007                                .secure(cookie_config.secure)
1008                                .same_site(cookie_config.same_site)
1009                                .path("/")
1010                                .finish();
1011
1012                            // Update token cookie with a new token
1013                            match resp.response_mut().add_cookie(&new_token_cookie) {
1014                                Ok(_) => {}
1015                                Err(e) => {
1016                                    let res = HttpResponse::InternalServerError()
1017                                        .body("Failed to set a new csrf token");
1018
1019                                    error!(
1020                                        "unable to set a token cookie in csrf response: {:?}",
1021                                        e
1022                                    );
1023                                    return Poll::Ready(Ok(resp
1024                                        .into_response(res)
1025                                        .map_into_boxed_body()
1026                                        .map_into_right_body()));
1027                                }
1028                            }
1029                        }
1030                    }
1031                }
1032
1033                Poll::Ready(Ok(resp.map_into_left_body()))
1034            }
1035            Err(err) => Poll::Ready(Err(err)),
1036        }
1037    }
1038}
1039
1040#[derive(Clone)]
1041pub struct CsrfToken(pub String);
1042
1043impl FromRequest for CsrfToken {
1044    type Error = Error;
1045    type Future = Ready<Result<Self, Self::Error>>;
1046
1047    fn from_request(req: &HttpRequest, _: &mut actix_web::dev::Payload) -> Self::Future {
1048        match req.extensions().get::<CsrfToken>() {
1049            Some(token) => ok(token.clone()),
1050            None => err(actix_web::error::ErrorInternalServerError(
1051                "CSRF middleware is not configured",
1052            )),
1053        }
1054    }
1055}
1056
1057pub fn generate_random_token() -> String {
1058    let mut buf = [0u8; TOKEN_LEN];
1059    rand::rng().fill_bytes(&mut buf);
1060    URL_SAFE_NO_PAD.encode(buf)
1061}
1062
1063pub fn eq_tokens(token_a: &[u8], token_b: &[u8]) -> bool {
1064    token_a.ct_eq(token_b).unwrap_u8() == 1
1065}
1066
1067pub fn generate_hmac_token_ctx(class: TokenClass, id: &str, secret: &[u8]) -> String {
1068    let tok = generate_random_token();
1069    let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC can take key of any size");
1070    mac.update(class.as_str().as_bytes());
1071    mac.update(b"|");
1072    mac.update(id.as_bytes());
1073    mac.update(b"|");
1074    mac.update(tok.as_bytes());
1075    let hmac_hex = hex::encode(mac.finalize().into_bytes());
1076    format!("{}.{}", hmac_hex, tok)
1077}
1078
1079fn encode_pre_session_cookie(id: &str, secret: &[u8]) -> String {
1080    let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC can take key of any size");
1081    mac.update(b"pre|");
1082    mac.update(id.as_bytes());
1083
1084    let sig = hex::encode(mac.finalize().into_bytes());
1085    format!("{}.{}", sig, id)
1086}
1087
1088fn decode_pre_session_cookie(val: &str, secret: &[u8]) -> Option<String> {
1089    let parts: Vec<&str> = val.split('.').collect();
1090    if parts.len() != 2 {
1091        return None;
1092    }
1093
1094    let (sig_hex, id) = (parts[0], parts[1]);
1095    let sig_bytes = hex::decode(sig_hex).ok()?;
1096
1097    let mut mac = Hmac::<Sha256>::new_from_slice(secret).ok()?;
1098    mac.update(b"pre|");
1099    mac.update(id.as_bytes());
1100
1101    let expected = mac.finalize().into_bytes();
1102
1103    if eq_tokens(&expected, &sig_bytes) {
1104        Some(id.to_string())
1105    } else {
1106        None
1107    }
1108}
1109
1110pub fn validate_hmac_token_ctx(
1111    class: TokenClass,
1112    id: &str,
1113    token: &[u8],
1114    secret: &[u8],
1115) -> Result<bool, Error> {
1116    let token_str = std::str::from_utf8(token)?;
1117    let parts: Vec<&str> = token_str.split('.').collect();
1118    if parts.len() != 2 {
1119        return Ok(false);
1120    }
1121
1122    let (hmac_hex, csrf_token) = (parts[0], parts[1]);
1123    let hmac_bytes = hex::decode(hmac_hex).map_err(actix_web::error::ErrorInternalServerError)?;
1124
1125    let mut mac = Hmac::<Sha256>::new_from_slice(secret)
1126        .map_err(actix_web::error::ErrorInternalServerError)?;
1127    mac.update(class.as_str().as_bytes());
1128    mac.update(b"|");
1129    mac.update(id.as_bytes());
1130    mac.update(b"|");
1131    mac.update(csrf_token.as_bytes());
1132
1133    let expected_hmac = mac.finalize().into_bytes();
1134
1135    Ok(eq_tokens(&expected_hmac, &hmac_bytes))
1136}
1137
1138/// Test util: validates an authorized token
1139pub fn validate_hmac_token(session_id: &str, token: &[u8], secret: &[u8]) -> Result<bool, Error> {
1140    validate_hmac_token_ctx(TokenClass::Authorized, session_id, token, secret)
1141}
1142
1143/// Extension trait for Actix HttpRequest to rotate
1144/// CSRF token without passing the config explicitly.
1145pub trait CsrfRequestExt {
1146    fn rotate_csrf_token_in_response(&self, resp: &mut HttpResponseBuilder) -> Result<(), Error>;
1147}
1148
1149impl CsrfRequestExt for HttpRequest {
1150    fn rotate_csrf_token_in_response(&self, resp: &mut HttpResponseBuilder) -> Result<(), Error> {
1151        // Clone out the config to avoid holding an extensions borrow across session access
1152        let cfg_rc: Rc<CsrfMiddlewareConfig> =
1153            match self.extensions().get::<Rc<CsrfMiddlewareConfig>>() {
1154                Some(cfg_rc_ref) => cfg_rc_ref.clone(),
1155                None => {
1156                    return Err(actix_web::error::ErrorInternalServerError(
1157                        "CSRF middleware config not found in request extensions",
1158                    ))
1159                }
1160            };
1161
1162        rotate_csrf_token_in_response(self, resp, cfg_rc.as_ref())
1163    }
1164}
1165
1166pub fn rotate_csrf_token_in_response(
1167    req: &HttpRequest,
1168    resp: &mut HttpResponseBuilder,
1169    config: &CsrfMiddlewareConfig,
1170) -> Result<(), Error> {
1171    // Always expire the pre-session cookie (best-effort) with strict flags
1172    let mut del = Cookie::new(CSRF_PRE_SESSION_KEY.to_string(), "");
1173    del.set_max_age(time::Duration::seconds(0));
1174    del.set_expires(time::OffsetDateTime::UNIX_EPOCH);
1175    del.set_path("/");
1176    del.set_http_only(PRE_SESSION_HTTP_ONLY);
1177    del.set_secure(PRE_SESSION_SECURE);
1178    del.set_same_site(PRE_SESSION_SAME_SITE);
1179    resp.cookie(del);
1180
1181    match config.pattern {
1182        #[cfg(feature = "actix-session")]
1183        CsrfPattern::SynchronizerToken => {
1184            let session = req.get_session();
1185            let new_token = generate_random_token();
1186
1187            let _ = session.remove(&config.anon_session_key_name);
1188
1189            session
1190                .insert(&config.token_cookie_name, new_token)
1191                .map_err(|_| {
1192                    actix_web::error::ErrorInternalServerError(
1193                        "Failed to rotate CSRF token in session",
1194                    )
1195                })?;
1196
1197            Ok(())
1198        }
1199        CsrfPattern::DoubleSubmitCookie => {
1200            // Need the session id from cookie
1201            let session_id = req
1202                .cookie(&config.session_id_cookie_name)
1203                .map(|c| c.value().to_string())
1204                .ok_or_else(|| {
1205                    actix_web::error::ErrorBadRequest("Missing session id cookie for CSRF rotation")
1206                })?;
1207
1208            let token = generate_hmac_token_ctx(
1209                TokenClass::Authorized,
1210                &session_id,
1211                config.secret_key.as_ref(),
1212            );
1213
1214            let (http_only, secure, same_site) = match &config.token_cookie_config {
1215                Some(cfg) => (cfg.http_only, cfg.secure, cfg.same_site),
1216                None => (true, true, SameSite::Lax),
1217            };
1218
1219            let csrf_cookie = Cookie::build(&config.token_cookie_name, token)
1220                .http_only(http_only)
1221                .secure(secure)
1222                .same_site(same_site)
1223                .path("/")
1224                .finish();
1225
1226            // Also expire anonymous token cookie
1227            let mut del_anon = Cookie::new(config.anon_token_cookie_name.clone(), "");
1228            del_anon.set_max_age(time::Duration::seconds(0));
1229            del_anon.set_expires(time::OffsetDateTime::UNIX_EPOCH);
1230            del_anon.set_path("/");
1231
1232            resp.cookie(csrf_cookie);
1233            resp.cookie(del_anon);
1234
1235            Ok(())
1236        }
1237    }
1238}
1239
1240fn check_secret_key(secret_key: &[u8]) {
1241    if secret_key.len() < 16 {
1242        warn!("csrf secret_key is too short (<16 bytes). Use at least 32 bytes for production.");
1243    } else if secret_key.len() < 32 {
1244        warn!("csrf secret_key is shorter than 32 bytes; recommend >=32 bytes.");
1245    }
1246}
1247
1248fn origin_allowed(headers: &HeaderMap, cfg: &CsrfMiddlewareConfig) -> bool {
1249    if !cfg.enforce_origin {
1250        return true;
1251    }
1252
1253    if cfg.allowed_origins.is_empty() {
1254        return false;
1255    }
1256
1257    // Try Origin header first
1258    if let Some(origin) = headers.get(header::ORIGIN).and_then(|hv| hv.to_str().ok()) {
1259        if cfg.allowed_origins.iter().any(|o| o == origin) {
1260            return true;
1261        }
1262
1263        return false;
1264    }
1265
1266    // Fallback requires it starts with allowed origin
1267    if let Some(referer) = headers.get(header::REFERER).and_then(|hv| hv.to_str().ok()) {
1268        if cfg.allowed_origins.iter().any(|o| referer.starts_with(o)) {
1269            return true;
1270        }
1271
1272        return false;
1273    }
1274
1275    false
1276}
1277
1278#[cfg(test)]
1279mod tests {
1280    use super::*;
1281
1282    const SESSION_ID: &str = "test";
1283    const SECRET: &str = "secret";
1284
1285    #[test]
1286    fn test_generate_and_validate_hmac_token() {
1287        let token = generate_hmac_token_ctx(TokenClass::Authorized, SESSION_ID, SECRET.as_bytes());
1288        let res = validate_hmac_token_ctx(
1289            TokenClass::Authorized,
1290            SESSION_ID,
1291            token.as_bytes(),
1292            SECRET.as_bytes(),
1293        );
1294        assert!(res.unwrap());
1295    }
1296
1297    #[test]
1298    fn test_handle_invalid_hmac_token() {
1299        let token = generate_hmac_token_ctx(TokenClass::Authorized, SESSION_ID, SECRET.as_bytes());
1300        let res = validate_hmac_token_ctx(
1301            TokenClass::Authorized,
1302            SESSION_ID,
1303            token.as_bytes(),
1304            SECRET.as_bytes(),
1305        );
1306        assert!(res.unwrap());
1307    }
1308}