1use http::{Request, StatusCode};
3use security_rust::Scanner;
4use std::future::Future;
5use std::pin::Pin;
6use std::sync::Arc;
7use std::task::{Context as TaskCtx, Poll};
8
9pub use security_rust::{AttackCategory, DetectionResult, ScannerBuilder, Severity};
10
11#[derive(Debug, thiserror::Error)]
12pub enum SecurityError {
13 #[error("attack blocked: {0}")]
14 AttackBlocked(String),
15 #[error("request body too large")]
17 BodyTooLarge,
18 #[error("inner error: {0}")]
19 Inner(#[from] Box<dyn std::error::Error + Send + Sync>),
20}
21
22impl SecurityError {
23 pub fn to_http_status(&self) -> StatusCode {
24 match self {
25 Self::AttackBlocked(_) => StatusCode::FORBIDDEN,
26 Self::BodyTooLarge => StatusCode::PAYLOAD_TOO_LARGE,
27 Self::Inner(_) => StatusCode::INTERNAL_SERVER_ERROR,
28 }
29 }
30}
31
32impl axum::response::IntoResponse for SecurityError {
36 fn into_response(self) -> axum::response::Response {
37 let status = self.to_http_status();
38 let body = match &self {
39 Self::AttackBlocked(types) => {
40 format!(r#"{{"error":"attack blocked","types":"{types}"}}"#)
41 }
42 Self::BodyTooLarge => r#"{"error":"request body too large"}"#.to_string(),
43 Self::Inner(e) => {
44 tracing::error!(error = %e, "security middleware internal error");
45 r#"{"error":"internal server error"}"#.to_string()
46 }
47 };
48 (status, body).into_response()
49 }
50}
51
52pub struct SecurityScanner {
54 scanner: Scanner,
55}
56
57impl SecurityScanner {
58 pub fn new() -> Self {
60 Self {
61 scanner: Scanner::default(),
62 }
63 }
64
65 pub fn scan(&self, input: &str) -> Vec<DetectionResult> {
67 self.scanner.scan(input)
68 }
69
70 pub fn scan_parts(&self, parts: &[&str]) -> Vec<DetectionResult> {
72 let mut results = Vec::with_capacity(parts.len() * 2);
73 for part in parts {
74 results.extend(self.scanner.scan(part));
75 }
76 results
77 }
78
79 pub fn scan_body(&self, body: &[u8]) -> Vec<DetectionResult> {
81 if let Ok(s) = std::str::from_utf8(body) {
82 self.scanner.scan(s)
83 } else {
84 Vec::new()
85 }
86 }
87}
88
89impl Default for SecurityScanner {
90 fn default() -> Self {
91 Self::new()
92 }
93}
94
95fn evaluate(results: &[DetectionResult]) -> Option<SecurityError> {
98 let mut blocked = false;
99 for r in results {
100 tracing::warn!(
101 attack_type = %r.attack_type,
102 category = ?r.category,
103 severity = ?r.severity,
104 matched = %r.matched_pattern,
105 "attack detected"
106 );
107 if r.attack_type != "jwt_attack"
111 && matches!(r.severity, Severity::High | Severity::Critical)
112 {
113 blocked = true;
114 }
115 }
116 if blocked {
117 let attack_types: Vec<String> = results.iter().map(|r| r.attack_type.to_string()).collect();
118 return Some(SecurityError::AttackBlocked(attack_types.join(", ")));
119 }
120 None
121}
122
123fn percent_decode(s: &str) -> String {
126 let bytes = s.as_bytes();
127 let mut out = Vec::with_capacity(bytes.len());
128 let mut i = 0;
129 while i < bytes.len() {
130 if bytes[i] == b'%'
131 && i + 2 < bytes.len()
132 && let (Some(h), Some(l)) = (hex_val(bytes[i + 1]), hex_val(bytes[i + 2]))
133 {
134 out.push(h * 16 + l);
135 i += 3;
136 continue;
137 }
138 out.push(bytes[i]);
139 i += 1;
140 }
141 String::from_utf8_lossy(&out).into_owned()
142}
143
144fn hex_val(b: u8) -> Option<u8> {
145 match b {
146 b'0'..=b'9' => Some(b - b'0'),
147 b'a'..=b'f' => Some(b - b'a' + 10),
148 b'A'..=b'F' => Some(b - b'A' + 10),
149 _ => None,
150 }
151}
152
153fn is_proxy_header(name: &http::header::HeaderName) -> bool {
161 matches!(
162 name.as_str(),
163 "x-forwarded-for"
164 | "x-real-ip"
165 | "forwarded"
166 | "x-forwarded-host"
167 | "x-forwarded-proto"
168 | "x-forwarded-port"
169 | "authorization"
170 )
171}
172
173fn request_parts<B>(req: &Request<B>) -> Vec<String> {
174 let mut parts: Vec<String> = Vec::new();
175 parts.push(percent_decode(&req.uri().to_string()));
176 for (name, value) in req.headers() {
177 if is_proxy_header(name) {
178 continue;
179 }
180 if let Ok(v) = value.to_str() {
181 parts.push(v.to_string());
182 }
183 }
184 parts
185}
186
187#[derive(Clone)]
190pub struct SecurityLayer {
191 scanner: Arc<SecurityScanner>,
192}
193
194impl SecurityLayer {
195 pub fn new() -> Self {
196 Self {
197 scanner: Arc::new(SecurityScanner::new()),
198 }
199 }
200}
201
202impl Default for SecurityLayer {
203 fn default() -> Self {
204 Self::new()
205 }
206}
207
208impl<S> tower::Layer<S> for SecurityLayer {
209 type Service = SecurityService<S>;
210
211 fn layer(&self, inner: S) -> Self::Service {
212 SecurityService {
213 inner,
214 scanner: Arc::clone(&self.scanner),
215 }
216 }
217}
218
219#[derive(Clone)]
220pub struct SecurityService<S> {
221 inner: S,
222 scanner: Arc<SecurityScanner>,
223}
224
225impl<S, B> tower::Service<Request<B>> for SecurityService<S>
226where
227 S: tower::Service<Request<B>> + Send + 'static,
228 S::Future: Send + 'static,
229 S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
230 S::Response: From<axum::response::Response> + Send,
233 B: Send + 'static,
234{
235 type Response = S::Response;
236 type Error = SecurityError;
237 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
238
239 fn poll_ready(&mut self, cx: &mut TaskCtx<'_>) -> Poll<Result<(), Self::Error>> {
240 self.inner
241 .poll_ready(cx)
242 .map_err(|e| SecurityError::Inner(e.into()))
243 }
244
245 fn call(&mut self, req: Request<B>) -> Self::Future {
246 let scanner = Arc::clone(&self.scanner);
247 let parts = request_parts(&req);
248 let strings: Vec<&str> = parts.iter().map(|s| s.as_str()).collect();
249 let results = scanner.scan_parts(&strings);
250
251 if let Some(err) = evaluate(&results) {
252 use axum::response::IntoResponse;
253 let resp: S::Response = err.into_response().into();
254 return Box::pin(async move { Ok(resp) });
255 }
256
257 let fut = self.inner.call(req);
258 Box::pin(async move { fut.await.map_err(|e| SecurityError::Inner(e.into())) })
259 }
260}
261
262#[derive(Clone)]
271pub struct SecurityBodyLayer {
272 scanner: Arc<SecurityScanner>,
273 body_limit: usize,
274}
275
276impl SecurityBodyLayer {
277 pub fn new() -> Self {
278 Self {
279 scanner: Arc::new(SecurityScanner::new()),
280 body_limit: 10 * 1024 * 1024,
281 }
282 }
283
284 pub fn body_limit(mut self, limit: usize) -> Self {
288 self.body_limit = limit;
289 self
290 }
291}
292
293impl Default for SecurityBodyLayer {
294 fn default() -> Self {
295 Self::new()
296 }
297}
298
299impl<S> tower::Layer<S> for SecurityBodyLayer {
300 type Service = SecurityBodyService<S>;
301
302 fn layer(&self, inner: S) -> Self::Service {
303 SecurityBodyService {
304 inner,
305 scanner: Arc::clone(&self.scanner),
306 body_limit: self.body_limit,
307 }
308 }
309}
310
311#[derive(Clone)]
312pub struct SecurityBodyService<S> {
313 inner: S,
314 scanner: Arc<SecurityScanner>,
315 body_limit: usize,
316}
317
318impl<S> tower::Service<Request<axum::body::Body>> for SecurityBodyService<S>
319where
320 S: tower::Service<Request<axum::body::Body>> + Clone + Send + 'static,
321 S::Future: Send + 'static,
322 S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
323{
324 type Response = S::Response;
325 type Error = SecurityError;
326 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
327
328 fn poll_ready(&mut self, cx: &mut TaskCtx<'_>) -> Poll<Result<(), Self::Error>> {
329 self.inner
330 .poll_ready(cx)
331 .map_err(|e| SecurityError::Inner(e.into()))
332 }
333
334 fn call(&mut self, req: Request<axum::body::Body>) -> Self::Future {
335 let scanner = Arc::clone(&self.scanner);
336 let body_limit = self.body_limit;
337 let mut inner = self.inner.clone();
338 let parts = request_parts(&req);
339 let strings: Vec<&str> = parts.iter().map(|s| s.as_str()).collect();
340 let header_results = scanner.scan_parts(&strings);
341
342 Box::pin(async move {
343 let (parts, body) = req.into_parts();
344 let bytes = match axum::body::to_bytes(body, body_limit).await {
347 Ok(bytes) => bytes,
348 Err(e) => {
349 let inner: Box<dyn std::error::Error + Send + Sync> = e.into_inner();
351 if inner
352 .downcast_ref::<http_body_util::LengthLimitError>()
353 .is_some()
354 {
355 return Err(SecurityError::BodyTooLarge);
356 }
357 return Err(SecurityError::Inner(inner));
358 }
359 };
360
361 let mut results = header_results;
362 results.extend(scanner.scan_body(&bytes));
363
364 if let Some(err) = evaluate(&results) {
365 return Err(err);
366 }
367
368 let req = Request::from_parts(parts, axum::body::Body::from(bytes));
369 inner
370 .call(req)
371 .await
372 .map_err(|e| SecurityError::Inner(e.into()))
373 })
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380
381 #[test]
382 fn scanner_detects_sql_injection() {
383 let s = SecurityScanner::new();
384 let results = s.scan("SELECT * FROM users; DROP TABLE users;");
385 assert!(!results.is_empty());
386 }
387
388 #[test]
389 fn scanner_detects_xss() {
390 let s = SecurityScanner::new();
391 let results = s.scan("<script>alert('xss')</script>");
392 assert!(!results.is_empty());
393 }
394
395 #[test]
396 fn scanner_clean_input_no_detection() {
397 let s = SecurityScanner::new();
398 let results = s.scan("hello world");
399 assert!(results.is_empty());
400 }
401
402 #[test]
403 fn scanner_scan_parts_aggregates() {
404 let s = SecurityScanner::new();
405 let results = s.scan_parts(&["clean", "<script>x</script>"]);
406 assert!(!results.is_empty());
407 }
408
409 #[test]
410 fn attack_blocked_maps_to_403() {
411 use axum::response::IntoResponse;
412 let resp = SecurityError::AttackBlocked("sqli".into()).into_response();
413 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
414 }
415
416 #[test]
417 fn inner_error_maps_to_500() {
418 use axum::response::IntoResponse;
419 let resp = SecurityError::Inner(Box::new(std::io::Error::other("boom"))).into_response();
420 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
421 }
422
423 #[test]
424 fn layer_constructs() {
425 let _layer = SecurityLayer::new();
426 }
427
428 #[test]
429 fn layer_default_constructs() {
430 let _layer: SecurityLayer = Default::default();
431 }
432
433 #[test]
434 fn body_layer_constructs() {
435 let _layer = SecurityBodyLayer::new().body_limit(1024);
436 }
437
438 #[test]
439 fn body_layer_default_constructs() {
440 let _layer: SecurityBodyLayer = Default::default();
441 }
442
443 #[tokio::test]
444 async fn body_layer_blocks_attack_in_body() {
445 use tower::Layer as _;
446 use tower::ServiceExt;
447
448 let layer = SecurityBodyLayer::new();
449 let svc = layer.layer(tower::service_fn(|_: Request<axum::body::Body>| async {
450 Ok::<_, std::convert::Infallible>(http::Response::new(axum::body::Body::empty()))
451 }));
452
453 let req = http::Request::builder()
454 .method("POST")
455 .uri("/submit")
456 .body(axum::body::Body::from("<script>alert('xss')</script>"))
457 .unwrap();
458 let result = svc.oneshot(req).await;
459 assert!(matches!(result, Err(SecurityError::AttackBlocked(_))));
460 }
461
462 #[tokio::test]
463 async fn body_over_limit_maps_to_413() {
464 use tower::Layer as _;
465 use tower::ServiceExt;
466
467 let layer = SecurityBodyLayer::new().body_limit(8);
468 let svc = layer.layer(tower::service_fn(|_: Request<axum::body::Body>| async {
469 Ok::<_, std::convert::Infallible>(http::Response::new(axum::body::Body::empty()))
470 }));
471
472 let req = http::Request::builder()
473 .method("POST")
474 .uri("/submit")
475 .body(axum::body::Body::from("x".repeat(64)))
476 .unwrap();
477 let result = svc.oneshot(req).await;
478 assert!(matches!(result, Err(SecurityError::BodyTooLarge)));
479 assert_eq!(
480 SecurityError::BodyTooLarge.to_http_status(),
481 StatusCode::PAYLOAD_TOO_LARGE
482 );
483 }
484
485 #[tokio::test]
486 async fn inner_error_response_body_does_not_leak_detail() {
487 use axum::response::IntoResponse;
488 let resp = SecurityError::Inner(Box::new(std::io::Error::other(
489 "secret-db-dsn=s3://user:pass",
490 )))
491 .into_response();
492 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
493 let (_, body) = resp.into_parts();
494 let bytes = axum::body::to_bytes(body, 4096).await.unwrap();
495 let text = String::from_utf8_lossy(&bytes);
496 assert!(!text.contains("secret-db-dsn"), "leaked detail: {text}");
497 assert!(text.contains("internal server error"), "got: {text}");
498 }
499
500 #[tokio::test]
501 async fn attack_blocked_response_body_shape() {
502 use axum::response::IntoResponse;
503 let resp = SecurityError::AttackBlocked("sqli, xss".into()).into_response();
504 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
505 let (_, body) = resp.into_parts();
506 let bytes = axum::body::to_bytes(body, 1024).await.unwrap();
507 let text = String::from_utf8_lossy(&bytes);
508 assert_eq!(
509 text,
510 r#"{"error":"attack blocked","types":"sqli, xss"}"#.to_string()
511 );
512 }
513
514 #[tokio::test]
515 async fn body_too_large_response_body_shape() {
516 use axum::response::IntoResponse;
517 let resp = SecurityError::BodyTooLarge.into_response();
518 assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
519 let (_, body) = resp.into_parts();
520 let bytes = axum::body::to_bytes(body, 1024).await.unwrap();
521 let text = String::from_utf8_lossy(&bytes);
522 assert_eq!(text, r#"{"error":"request body too large"}"#.to_string());
523 }
524
525 #[test]
526 fn request_parts_include_uri_and_headers() {
527 let req = http::Request::builder()
528 .uri("/search?q=abc")
529 .header("X-Custom", "val-1")
530 .body(())
531 .unwrap();
532 let parts = request_parts(&req);
533 assert_eq!(
534 parts,
535 vec!["/search?q=abc".to_string(), "val-1".to_string()]
536 );
537 }
538
539 #[test]
540 fn percent_decode_handles_encoded_sql_chars() {
541 assert_eq!(
542 percent_decode("/q?x=SELECT%20*%20FROM%20users"),
543 "/q?x=SELECT * FROM users"
544 );
545 assert_eq!(percent_decode("1%27%20OR%20%271%27%3D%271"), "1' OR '1'='1");
546 assert_eq!(
547 percent_decode("/clean?q=hello%20world"),
548 "/clean?q=hello world"
549 );
550 assert_eq!(percent_decode("%3cscript%3e"), "<script>");
551 assert_eq!(percent_decode("/%zz%"), "/%zz%");
553 assert_eq!(percent_decode("/100%25"), "/100%");
554 }
555
556 #[test]
557 fn request_parts_percent_decode_uri_only() {
558 let req = http::Request::builder()
559 .uri("/search?q=SELECT%20*%20FROM%20users")
560 .header("X-Custom", "val%201")
561 .body(())
562 .unwrap();
563 let parts = request_parts(&req);
564 assert_eq!(parts[0], "/search?q=SELECT * FROM users");
566 assert_eq!(parts[1], "val%201");
567 }
568
569 #[tokio::test]
570 async fn header_layer_blocks_attack_in_uri() {
571 use tower::Layer as _;
572 use tower::ServiceExt;
573
574 let layer = SecurityLayer::new();
575 let svc = layer.layer(tower::service_fn(|_: Request<()>| async {
576 Ok::<_, std::convert::Infallible>(http::Response::new(axum::body::Body::empty()))
577 }));
578
579 let req = http::Request::builder()
582 .uri("/redirect?url=javascript:alert(1)")
583 .body(())
584 .unwrap();
585 let resp = svc.oneshot(req).await.expect("blocked as response");
586 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
587 }
588
589 #[tokio::test]
590 async fn header_layer_blocks_attack_in_header() {
591 use tower::Layer as _;
592 use tower::ServiceExt;
593
594 let layer = SecurityLayer::new();
595 let svc = layer.layer(tower::service_fn(|_: Request<()>| async {
596 Ok::<_, std::convert::Infallible>(http::Response::new(axum::body::Body::empty()))
597 }));
598
599 let req = http::Request::builder()
600 .uri("/clean")
601 .header("X-Trace", "<script>alert(1)</script>")
602 .body(())
603 .unwrap();
604 let resp = svc.oneshot(req).await.expect("blocked as response");
605 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
606 }
607
608 #[tokio::test]
609 async fn header_layer_blocks_encoded_sqli_in_uri() {
610 use tower::Layer as _;
611 use tower::ServiceExt;
612
613 let layer = SecurityLayer::new();
614 let svc = layer.layer(tower::service_fn(|_: Request<()>| async {
615 Ok::<_, std::convert::Infallible>(http::Response::new(axum::body::Body::empty()))
616 }));
617
618 let req = http::Request::builder()
620 .uri("/search?q=SELECT%20*%20FROM%20users")
621 .body(())
622 .unwrap();
623 let resp = svc.oneshot(req).await.expect("blocked as response");
624 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
625 }
626
627 #[tokio::test]
628 async fn header_layer_blocks_encoded_single_quote_sqli_in_uri() {
629 use tower::Layer as _;
630 use tower::ServiceExt;
631
632 let layer = SecurityLayer::new();
633 let svc = layer.layer(tower::service_fn(|_: Request<()>| async {
634 Ok::<_, std::convert::Infallible>(http::Response::new(axum::body::Body::empty()))
635 }));
636
637 let req = http::Request::builder()
639 .uri("/login?u=1%27%20OR%20%271%27%3D%271")
640 .body(())
641 .unwrap();
642 let resp = svc.oneshot(req).await.expect("blocked as response");
643 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
644 }
645
646 #[tokio::test]
647 async fn header_layer_passes_clean_encoded_query_through() {
648 use tower::Layer as _;
649 use tower::ServiceExt;
650
651 let layer = SecurityLayer::new();
652 let svc = layer.layer(tower::service_fn(|req: Request<()>| async move {
653 Ok::<_, std::convert::Infallible>(http::Response::new(axum::body::Body::from(
654 req.uri().path().to_string(),
655 )))
656 }));
657
658 let req = http::Request::builder()
660 .uri("/search?q=hello%20world")
661 .body(())
662 .unwrap();
663 let resp = svc.oneshot(req).await.expect("clean request passes");
664 let (_, body) = resp.into_parts();
665 let bytes = axum::body::to_bytes(body, 1024).await.unwrap();
666 assert_eq!(String::from_utf8_lossy(&bytes), "/search");
667 }
668
669 #[tokio::test]
670 async fn header_layer_passes_proxy_headers_with_internal_ip() {
671 use tower::Layer as _;
672 use tower::ServiceExt;
673
674 let layer = SecurityLayer::new();
675 let svc = layer.layer(tower::service_fn(|req: Request<()>| async move {
676 Ok::<_, std::convert::Infallible>(http::Response::new(axum::body::Body::from(
677 req.uri().path().to_string(),
678 )))
679 }));
680
681 let req = http::Request::builder()
683 .uri("/api/v1/booking/dates?region_id=1")
684 .header("X-Real-IP", "172.19.0.1")
685 .header("X-Forwarded-For", "172.19.0.1")
686 .body(())
687 .unwrap();
688 let resp = svc.oneshot(req).await.expect("proxy headers pass");
689 assert_eq!(resp.status(), StatusCode::OK);
690 }
691
692 #[tokio::test]
693 async fn header_layer_passes_clean_request_through() {
694 use tower::Layer as _;
695 use tower::ServiceExt;
696
697 let layer = SecurityLayer::new();
698 let svc = layer.layer(tower::service_fn(|req: Request<()>| async move {
699 Ok::<_, std::convert::Infallible>(http::Response::new(axum::body::Body::from(
700 req.uri().path().to_string(),
701 )))
702 }));
703
704 let req = http::Request::builder().uri("/clean").body(()).unwrap();
705 let resp = svc.oneshot(req).await.expect("clean request passes");
706 let (_, body) = resp.into_parts();
707 let bytes = axum::body::to_bytes(body, 1024).await.unwrap();
708 assert_eq!(String::from_utf8_lossy(&bytes), "/clean");
709 }
710
711 #[tokio::test]
712 async fn body_layer_passes_clean_body_through() {
713 use tower::Layer as _;
714 use tower::ServiceExt;
715
716 let layer = SecurityBodyLayer::new();
717 let svc = layer.layer(tower::service_fn(
718 |req: Request<axum::body::Body>| async move {
719 let (_, body) = req.into_parts();
720 let bytes = axum::body::to_bytes(body, 1024).await.unwrap();
721 Ok::<_, std::convert::Infallible>(http::Response::new(axum::body::Body::from(
722 bytes,
723 )))
724 },
725 ));
726
727 let req = http::Request::builder()
728 .method("POST")
729 .uri("/submit")
730 .body(axum::body::Body::from("hello world"))
731 .unwrap();
732 let resp = svc.oneshot(req).await.expect("clean body passes through");
733 let (_, body) = resp.into_parts();
734 let bytes = axum::body::to_bytes(body, 1024).await.unwrap();
735 assert_eq!(&bytes[..], b"hello world");
736 }
737}