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
35const PRE_SESSION_HTTP_ONLY: bool = true;
37const PRE_SESSION_SECURE: bool = true;
38const PRE_SESSION_SAME_SITE: SameSite = SameSite::Strict;
39
40pub const DEFAULT_CSRF_TOKEN_KEY: &str = "CSRF";
42
43pub const DEFAULT_CSRF_ANON_TOKEN_KEY: &str = "CSRF-ANON";
45
46pub const DEFAULT_CSRF_TOKEN_FIELD: &str = "csrf_token";
49pub const DEFAULT_CSRF_TOKEN_HEADER: &str = "X-CSRF-Token";
50
51pub const DEFAULT_SESSION_ID_KEY: &str = "id";
56
57pub const CSRF_PRE_SESSION_KEY: &str = "pre-session";
61
62const TOKEN_LEN: usize = 32; type 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#[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 pub token_cookie_name: String,
107 pub anon_token_cookie_name: String,
109 #[cfg(feature = "actix-session")]
110 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 pub enforce_origin: bool,
119 pub allowed_origins: Vec<String>,
121 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, }
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, 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 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 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_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 #[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 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 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 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 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 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 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 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 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 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 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 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 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(), client_token: token.into_bytes(),
511 session_id,
512 token_class,
513 req: Some(req),
514 });
515 }
516
517 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 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 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 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(); 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 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 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 self.set(next_state);
766
767 cx.waker().wake_by_ref(); 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 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 *this.remove_anon_session {
878 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 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 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 if *this.remove_anon_session {
952 let _ = resp
953 .request()
954 .get_session()
955 .remove(&config.anon_session_key_name);
956 }
957
958 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 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 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
1138pub 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
1143pub 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 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 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 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 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 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 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}