Skip to main content

rama_http/layer/csrf/
service.rs

1use 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/// Middleware that enforces cross-origin request forgery (CSRF) protection.
17///
18/// See the [module docs](crate::layer::csrf) for more details.
19#[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    /// Verify a request against the configured CSRF protection.
46    pub(super) fn verify<Body>(&self, req: &Request<Body>) -> Result<(), ProtectionError> {
47        // RFC 9110 §9.2.1 safe-ish set used by the reference: only GET/HEAD/OPTIONS are exempt
48        // (deliberately not `Method::is_safe`, which also exempts TRACE and QUERY). QUERY
49        // (RFC 10008) is safe yet intentionally left out: it is not CORS-safelisted, so browsers
50        // always preflight it and send `Origin`/`Sec-Fetch-Site`, letting the cross-origin checks
51        // below protect it like any body-bearing method instead of blanket-exempting it.
52        if matches!(
53            req.method(),
54            &crate::Method::GET | &crate::Method::HEAD | &crate::Method::OPTIONS
55        ) {
56            return Ok(());
57        }
58
59        // The usable request origin (present, not `null`, with an `http`/`https` scheme).
60        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            // Absent or unrecognized: fall through to the Origin/Host check.
91            Some(SecFetchSite::Unknown(_)) | None => {}
92        }
93
94        // No usable cross-origin signal at all → same-origin or non-browser request.
95        if req
96            .headers()
97            .get(header::ORIGIN)
98            .is_none_or(|value| value.is_empty())
99        {
100            return Ok(());
101        }
102
103        // Origin is present; compare its authority to the request's effective host. Per RFC 7230
104        // §5.3 the effective host is the request-target authority if present, else the `Host`
105        // header.
106        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                // Attach the cause so surrounding layers can inspect it regardless of the builder.
176                response.extensions().insert(err);
177                Ok(response)
178            }
179        }
180    }
181}