Skip to content
This repository was archived by the owner on May 31, 2026. It is now read-only.

Commit eb1ce98

Browse files
committed
feat: add bypass function
Not sure if there is a more idiomatic way to do this in Tower but a quick search did not show generic skipping mechanisms for middleware.
1 parent 2cc01d0 commit eb1ce98

2 files changed

Lines changed: 103 additions & 7 deletions

File tree

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ This boils down to (quoting from the blog):
1414

1515
The following features from the Go stdlib [implementation](https://cs.opensource.google/go/go/+/refs/tags/go1.25.0:src/net/http/csrf.go) are not implemented:
1616

17-
- Skipping the middleware: not sure if there is a idiomatic alternative in tower
1817
- Custom handlers for error responses: this is not idiomatic for tower
1918

2019
## Open issues

src/lib.rs

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::result::Result;
55
use std::sync::Arc;
66
use std::task::{Context, Poll};
77

8-
use http::{Method, Request, Response};
8+
use http::{Method, Request, Response, Uri};
99
use tower::{BoxError, Layer, Service};
1010
use url::Url;
1111

@@ -33,6 +33,30 @@ pub enum ProtectionError {
3333
SecFetchSiteUnexpectedValue(String),
3434
}
3535

36+
struct Bypass<T: Fn(&Method, &Uri) -> bool>(T);
37+
38+
impl<T: Fn(&Method, &Uri) -> bool> std::fmt::Debug for Bypass<T> {
39+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40+
f.debug_struct("<fn>").finish()
41+
}
42+
}
43+
44+
trait Filter: std::fmt::Debug + Send + Sync {
45+
fn is_bypassed(&self, method: &Method, uri: &Uri) -> bool;
46+
}
47+
48+
impl<T: Fn(&Method, &Uri) -> bool> Filter for Option<Bypass<T>>
49+
where
50+
T: Send + Sync,
51+
{
52+
fn is_bypassed(&self, method: &Method, uri: &Uri) -> bool {
53+
match self {
54+
Some(ref p) => p.0(method, uri),
55+
None => false,
56+
}
57+
}
58+
}
59+
3660
#[derive(Clone, Debug, Default)]
3761
struct Origins(Arc<HashSet<String>>);
3862

@@ -46,11 +70,21 @@ impl Origins {
4670
}
4771
}
4872

49-
#[derive(Clone, Debug, Default)]
73+
#[derive(Clone, Debug)]
5074
pub struct CrossOriginProtectionLayer {
75+
insecure_bypass: Arc<dyn Filter>,
5176
trusted_origins: Origins,
5277
}
5378

79+
impl Default for CrossOriginProtectionLayer {
80+
fn default() -> Self {
81+
CrossOriginProtectionLayer {
82+
insecure_bypass: Arc::new(Option::<Bypass<fn(&Method, &Uri) -> bool>>::default()),
83+
trusted_origins: Origins::default(),
84+
}
85+
}
86+
}
87+
5488
impl CrossOriginProtectionLayer {
5589
pub fn add_trusted_origin<S: Into<String>>(mut self, origin: S) -> Result<Self, ConfigError> {
5690
let origin = origin.into();
@@ -66,27 +100,47 @@ impl CrossOriginProtectionLayer {
66100

67101
Ok(self)
68102
}
103+
104+
pub fn with_insecure_bypass<F>(self, predicate: F) -> CrossOriginProtectionLayer
105+
where
106+
F: Fn(&Method, &Uri) -> bool + Send + Sync + 'static,
107+
{
108+
CrossOriginProtectionLayer {
109+
insecure_bypass: Arc::new(Some(Bypass(predicate))),
110+
trusted_origins: self.trusted_origins,
111+
}
112+
}
69113
}
70114

71115
impl<S> Layer<S> for CrossOriginProtectionLayer {
72116
type Service = CrossOriginProtectionMiddleware<S>;
73117

74118
fn layer(&self, inner: S) -> Self::Service {
75-
let trusted_origins = self.trusted_origins.clone();
76-
77119
CrossOriginProtectionMiddleware {
78120
inner,
79-
trusted_origins,
121+
insecure_bypass: self.insecure_bypass.clone(),
122+
trusted_origins: self.trusted_origins.clone(),
80123
}
81124
}
82125
}
83126

84-
#[derive(Clone, Debug, Default)]
127+
#[derive(Clone, Debug)]
85128
pub struct CrossOriginProtectionMiddleware<S> {
86129
inner: S,
130+
insecure_bypass: Arc<dyn Filter>,
87131
trusted_origins: Origins,
88132
}
89133

134+
impl<S: Default> Default for CrossOriginProtectionMiddleware<S> {
135+
fn default() -> Self {
136+
Self {
137+
inner: S::default(),
138+
insecure_bypass: Arc::new(Option::<Bypass<fn(&Method, &Uri) -> bool>>::default()),
139+
trusted_origins: Origins::default(),
140+
}
141+
}
142+
}
143+
90144
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for CrossOriginProtectionMiddleware<S>
91145
where
92146
S: Service<Request<ReqBody>, Response = Response<ResBody>> + Clone + Send + 'static,
@@ -116,6 +170,10 @@ where
116170

117171
impl<S> CrossOriginProtectionMiddleware<S> {
118172
fn verify<Body>(&self, req: &Request<Body>) -> Result<(), ProtectionError> {
173+
if self.insecure_bypass.is_bypassed(req.method(), req.uri()) {
174+
return Ok(());
175+
}
176+
119177
if matches!(*req.method(), Method::GET | Method::HEAD | Method::OPTIONS) {
120178
return Ok(());
121179
}
@@ -172,6 +230,27 @@ mod tests {
172230
use super::*;
173231
use http::Request;
174232

233+
#[test]
234+
fn test_middleware_debug_trait() {
235+
let layer = CrossOriginProtectionLayer::default();
236+
let middleware = layer
237+
.clone()
238+
.with_insecure_bypass(|method, uri| method == Method::POST && uri.path() == "/bypass")
239+
.layer(());
240+
241+
assert_eq!(
242+
format!("{:?}", middleware),
243+
"CrossOriginProtectionMiddleware { inner: (), insecure_bypass: Some(<fn>), trusted_origins: Origins({}) }"
244+
);
245+
246+
let middleware = layer.layer(());
247+
248+
assert_eq!(
249+
format!("{:?}", middleware),
250+
"CrossOriginProtectionMiddleware { inner: (), insecure_bypass: None, trusted_origins: Origins({}) }"
251+
);
252+
}
253+
175254
#[test]
176255
fn test_add_trusted_origin() {
177256
assert!(matches!(
@@ -306,6 +385,24 @@ mod tests {
306385
assert!(middleware.verify(&request).is_err());
307386
}
308387

388+
#[test]
389+
fn test_origin_mismatch_host_bypassed() {
390+
let layer = CrossOriginProtectionLayer::default();
391+
let middleware = layer
392+
.with_insecure_bypass(|method, uri| method == Method::POST && uri.path() == "/bypass")
393+
.layer(());
394+
395+
let request = Request::builder()
396+
.method("POST")
397+
.uri("/bypass")
398+
.header("origin", "https://evil.com")
399+
.header("host", "example.com")
400+
.body(())
401+
.unwrap();
402+
403+
assert!(middleware.verify(&request).is_ok());
404+
}
405+
309406
#[test]
310407
fn test_no_origin_no_sec_fetch_site_allowed() {
311408
let middleware: CrossOriginProtectionMiddleware<()> = Default::default();

0 commit comments

Comments
 (0)