rama_http/layer/csrf/
service.rs1use std::fmt::{self, Debug, Formatter};
2use std::sync::Arc;
3
4use rama_core::Service;
5use rama_core::extensions::ExtensionsRef;
6use rama_utils::macros::define_inner_service_accessors;
7
8use super::origin::{CsrfOrigin, Origins};
9use super::{
10 BypassFn, DebugFn, DefaultResponseForProtectionError, ProtectionError, ProtectionErrorKind,
11 ResponseForProtectionError,
12};
13use crate::headers::{HeaderMapExt as _, Host, Origin, SecFetchSite};
14use crate::{Request, Response, header};
15
16#[derive(Clone)]
20#[must_use]
21pub struct Csrf<S, T = DefaultResponseForProtectionError> {
22 inner: S,
23 insecure_bypass: Option<Arc<BypassFn>>,
24 rejection_response: T,
25 trusted_origins: Origins,
26}
27
28impl<S, T> Csrf<S, T> {
29 pub(super) fn new(
30 inner: S,
31 insecure_bypass: Option<Arc<BypassFn>>,
32 rejection_response: T,
33 trusted_origins: Origins,
34 ) -> Self {
35 Self {
36 inner,
37 insecure_bypass,
38 rejection_response,
39 trusted_origins,
40 }
41 }
42
43 define_inner_service_accessors!();
44
45 pub(super) fn verify<Body>(&self, req: &Request<Body>) -> Result<(), ProtectionError> {
47 if matches!(
53 req.method(),
54 &crate::Method::GET | &crate::Method::HEAD | &crate::Method::OPTIONS
55 ) {
56 return Ok(());
57 }
58
59 let csrf_origin = req
61 .headers()
62 .typed_get::<Origin>()
63 .filter(|origin| !origin.is_null())
64 .and_then(|origin| {
65 CsrfOrigin::from_parts(origin.scheme(), origin.hostname().as_ref(), origin.port())
66 });
67
68 let is_exempt = || {
69 if let Some(bypass) = self.insecure_bypass.as_ref()
70 && bypass(req.method(), req.uri())
71 {
72 return true;
73 }
74 csrf_origin
75 .as_ref()
76 .is_some_and(|origin| self.trusted_origins.contains(origin))
77 };
78
79 match req.headers().typed_get::<SecFetchSite>() {
80 Some(SecFetchSite::SameOrigin | SecFetchSite::None) => return Ok(()),
81 Some(SecFetchSite::CrossSite | SecFetchSite::SameSite) => {
82 return if is_exempt() {
83 Ok(())
84 } else {
85 Err(ProtectionError::new(
86 ProtectionErrorKind::CrossOriginRequest,
87 ))
88 };
89 }
90 Some(SecFetchSite::Unknown(_)) | None => {}
92 }
93
94 if req
96 .headers()
97 .get(header::ORIGIN)
98 .is_none_or(|value| value.is_empty())
99 {
100 return Ok(());
101 }
102
103 if let Some(origin) = csrf_origin.as_ref() {
107 let matched = if let Some(authority) = req.uri().authority() {
108 origin.matches_host(authority.host().to_str().as_ref(), authority.port_u16())
109 } else if let Some(host) = req.headers().typed_get::<Host>() {
110 origin.matches_host(&host.0.host.to_string(), host.0.port.into())
111 } else {
112 false
113 };
114 if matched {
115 return Ok(());
116 }
117 }
118
119 if is_exempt() {
120 return Ok(());
121 }
122
123 Err(ProtectionError::new(
124 ProtectionErrorKind::CrossOriginRequestFromOldBrowser,
125 ))
126 }
127}
128
129impl<S, T> Default for Csrf<S, T>
130where
131 S: Default,
132 T: Default,
133{
134 fn default() -> Self {
135 Self {
136 inner: S::default(),
137 insecure_bypass: None,
138 rejection_response: T::default(),
139 trusted_origins: Origins::default(),
140 }
141 }
142}
143
144impl<S: Debug, T> Debug for Csrf<S, T> {
145 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
146 f.debug_struct("Csrf")
147 .field("inner", &self.inner)
148 .field(
149 "insecure_bypass",
150 &self.insecure_bypass.as_ref().map(|_| DebugFn),
151 )
152 .field("trusted_origins", &self.trusted_origins)
153 .field("rejection_response", &DebugFn)
154 .finish()
155 }
156}
157
158impl<S, T, ReqBody, ResBody> Service<Request<ReqBody>> for Csrf<S, T>
159where
160 S: Service<Request<ReqBody>, Output = Response<ResBody>>,
161 T: ResponseForProtectionError<ResBody>,
162 ReqBody: Send + 'static,
163 ResBody: Send + 'static,
164{
165 type Output = Response<ResBody>;
166 type Error = S::Error;
167
168 async fn serve(&self, req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
169 match self.verify(&req) {
170 Ok(()) => self.inner.serve(req).await,
171 Err(err) => {
172 let response = self
173 .rejection_response
174 .response_for_protection_error(err.clone());
175 response.extensions().insert(err);
177 Ok(response)
178 }
179 }
180 }
181}