1use crate::types::Http1Error;
11use std::borrow::Cow;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum SmugglingKind {
16 ClTe,
18 TeCl,
20 TeTe,
22 HostInjection,
24 HeaderInjection,
26 DoubleContentLength,
28 DuplicateHost,
31}
32
33impl SmugglingKind {
34 #[inline]
36 pub fn as_str(&self) -> &'static str {
37 match self {
38 Self::ClTe => "CL.TE",
39 Self::TeCl => "TE.CL",
40 Self::TeTe => "TE.TE",
41 Self::HostInjection => "Host Injection",
42 Self::HeaderInjection => "Header Injection",
43 Self::DoubleContentLength => "Double Content-Length",
44 Self::DuplicateHost => "Duplicate Host",
45 }
46 }
47}
48
49#[derive(Debug, Default, Clone)]
53pub struct SmugglingDetector;
54
55impl SmugglingDetector {
56 #[inline]
58 pub fn new() -> Self {
59 Self
60 }
61
62 pub fn detect<N, V>(
76 &self,
77 headers: &[(N, V)],
78 ) -> Result<(), (SmugglingKind, String)>
79 where
80 N: AsRef<str>,
81 V: AsRef<str>,
82 {
83 self.detect_impl(headers, |s| Cow::Owned(s.to_ascii_lowercase()))
84 }
85
86 fn detect_impl<N, V>(
92 &self,
93 headers: &[(N, V)],
94 normalize: impl for<'a> Fn(&'a str) -> Cow<'a, str>,
95 ) -> Result<(), (SmugglingKind, String)>
96 where
97 N: AsRef<str>,
98 V: AsRef<str>,
99 {
100 let mut has_content_length = false;
101 let mut content_length_count: usize = 0;
102 let mut transfer_encoding_values: Vec<String> = Vec::with_capacity(4);
103 let mut has_te = false;
104 let mut host_count: usize = 0;
105 let mut cl_position: Option<usize> = None;
107 let mut te_position: Option<usize> = None;
108
109 for (idx, (name, value)) in headers.iter().enumerate() {
110 let value = value.as_ref();
111 let n = normalize(name.as_ref());
112
113 match &*n {
114 "content-length" => {
115 content_length_count += 1;
116 if content_length_count > 1 {
117 return Err((
118 SmugglingKind::DoubleContentLength,
119 "multiple Content-Length".into(),
120 ));
121 }
122 if !value.chars().all(|c| c.is_ascii_digit()) {
124 return Err((
125 SmugglingKind::ClTe,
126 "Content-Length not decimal".into(),
127 ));
128 }
129 has_content_length = true;
130 if cl_position.is_none() {
131 cl_position = Some(idx);
132 }
133 }
134 "transfer-encoding" => {
135 has_te = true;
136 if te_position.is_none() {
137 te_position = Some(idx);
138 }
139 for v in value.split(',') {
141 let v = v.trim().to_ascii_lowercase();
142 if !v.is_empty() {
143 transfer_encoding_values.push(v);
144 }
145 }
146 }
147 "host" => {
148 host_count += 1;
149 if host_count > 1 {
150 return Err((
152 SmugglingKind::DuplicateHost,
153 "multiple Host headers".into(),
154 ));
155 }
156 if value.contains('\r') || value.contains('\n') {
157 return Err((
158 SmugglingKind::HostInjection,
159 "host contains CRLF".into(),
160 ));
161 }
162 }
163 _ => {}
164 }
165
166 if value.contains("\r") || value.contains("\n") {
168 return Err((
169 SmugglingKind::HeaderInjection,
170 format!("header '{n}' value contains CRLF"),
171 ));
172 }
173 if Self::contains_invalid_control(value) {
175 return Err((
176 SmugglingKind::HeaderInjection,
177 format!("header '{n}' contains invalid control chars"),
178 ));
179 }
180 }
181
182 if has_content_length && has_te {
186 let kind = match (cl_position, te_position) {
187 (Some(cl), Some(te)) if te < cl => SmugglingKind::TeCl,
188 _ => SmugglingKind::ClTe,
189 };
190 return Err((
191 kind,
192 "Content-Length and Transfer-Encoding both present".into(),
193 ));
194 }
195
196 if has_te {
198 if transfer_encoding_values.iter().any(|v| v == "identity") {
202 return Err((
203 SmugglingKind::TeTe,
204 "deprecated 'identity' in Transfer-Encoding (RFC 7230)".into(),
205 ));
206 }
207 let chunked_positions: Vec<usize> = transfer_encoding_values
208 .iter()
209 .enumerate()
210 .filter_map(|(i, v)| (v == "chunked").then_some(i))
211 .collect();
212 if chunked_positions.len() > 1 {
213 return Err((
214 SmugglingKind::TeTe,
215 "multiple 'chunked' in Transfer-Encoding".into(),
216 ));
217 }
218 if let Some(pos) = chunked_positions.first().copied()
219 && pos != transfer_encoding_values.len() - 1 {
220 return Err((
221 SmugglingKind::TeTe,
222 "'chunked' not last in Transfer-Encoding".into(),
223 ));
224 }
225 if chunked_positions.is_empty() {
227 return Err((
228 SmugglingKind::TeTe,
229 "Transfer-Encoding without 'chunked'".into(),
230 ));
231 }
232 }
233
234 Ok(())
238 }
239
240 pub fn detect_already_lowercased<N, V>(
250 &self,
251 headers: &[(N, V)],
252 ) -> Result<(), (SmugglingKind, String)>
253 where
254 N: AsRef<str>,
255 V: AsRef<str>,
256 {
257 self.detect_impl(headers, |s| Cow::Borrowed(s))
258 }
259
260 pub fn detect_err<N, V>(
262 &self,
263 headers: &[(N, V)],
264 ) -> Result<(), Http1Error>
265 where
266 N: AsRef<str>,
267 V: AsRef<str>,
268 {
269 self.detect(headers).map_err(|(k, m)| {
270 Http1Error::SmugglingDetected(format!("{}: {}", k.as_str(), m))
271 })
272 }
273
274 pub fn detect_err_already_lowercased<N, V>(
279 &self,
280 headers: &[(N, V)],
281 ) -> Result<(), Http1Error>
282 where
283 N: AsRef<str>,
284 V: AsRef<str>,
285 {
286 self.detect_already_lowercased(headers).map_err(|(k, m)| {
287 Http1Error::SmugglingDetected(format!("{}: {}", k.as_str(), m))
288 })
289 }
290
291 #[inline]
293 fn contains_invalid_control(s: &str) -> bool {
294 s.chars().any(|c| {
298 let code = c as u32;
299 matches!(code, 0x00..=0x08 | 0x0b | 0x0c | 0x0e..=0x1f | 0x7f)
300 })
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn test_clean_headers() {
310 let d = SmugglingDetector::new();
311 let headers = vec![
312 ("Host".to_string(), "example.com".to_string()),
313 ("Content-Length".to_string(), "13".to_string()),
314 ("Accept".to_string(), "text/plain".to_string()),
315 ];
316 assert!(d.detect(&headers).is_ok());
317 }
318
319 #[test]
320 fn test_cl_te_smuggling() {
321 let d = SmugglingDetector::new();
322 let headers = vec![
323 ("Host".to_string(), "example.com".to_string()),
324 ("Content-Length".to_string(), "0".to_string()),
325 ("Transfer-Encoding".to_string(), "chunked".to_string()),
326 ];
327 let r = d.detect(&headers);
328 assert!(r.is_err());
329 assert_eq!(r.unwrap_err().0, SmugglingKind::ClTe);
330 }
331
332 #[test]
333 fn test_te_te_multiple_chunked() {
334 let d = SmugglingDetector::new();
335 let headers = vec![
336 ("Host".to_string(), "example.com".to_string()),
337 (
338 "Transfer-Encoding".to_string(),
339 "chunked, chunked".to_string(),
340 ),
341 ];
342 let r = d.detect(&headers);
343 assert_eq!(r.unwrap_err().0, SmugglingKind::TeTe);
344 }
345
346 #[test]
347 fn test_te_te_chunked_not_last() {
348 let d = SmugglingDetector::new();
349 let headers = vec![
350 ("Host".to_string(), "example.com".to_string()),
351 (
352 "Transfer-Encoding".to_string(),
353 "chunked, identity".to_string(),
354 ),
355 ];
356 let r = d.detect(&headers);
357 assert_eq!(r.unwrap_err().0, SmugglingKind::TeTe);
358 }
359
360 #[test]
361 fn test_double_content_length() {
362 let d = SmugglingDetector::new();
363 let headers = vec![
364 ("Host".to_string(), "example.com".to_string()),
365 ("Content-Length".to_string(), "10".to_string()),
366 ("Content-Length".to_string(), "20".to_string()),
367 ];
368 let r = d.detect(&headers);
369 assert_eq!(r.unwrap_err().0, SmugglingKind::DoubleContentLength);
370 }
371
372 #[test]
373 fn test_header_injection_crlf() {
374 let d = SmugglingDetector::new();
375 let headers = vec![
376 ("Host".to_string(), "example.com".to_string()),
377 ("X-Test".to_string(), "val\r\nEvil: yes".to_string()),
378 ];
379 let r = d.detect(&headers);
380 assert_eq!(r.unwrap_err().0, SmugglingKind::HeaderInjection);
381 }
382
383 #[test]
384 fn test_host_injection() {
385 let d = SmugglingDetector::new();
386 let headers = vec![("Host".to_string(), "example.com\r\nX: y".to_string())];
387 let r = d.detect(&headers);
388 assert_eq!(r.unwrap_err().0, SmugglingKind::HostInjection);
389 }
390
391 #[test]
392 fn test_duplicate_host_rejected() {
393 let d = SmugglingDetector::new();
395 let headers = vec![
396 ("Host".to_string(), "a.com".to_string()),
397 ("Host".to_string(), "b.com".to_string()),
398 ];
399 let r = d.detect(&headers);
400 assert_eq!(r.unwrap_err().0, SmugglingKind::DuplicateHost);
401 }
402
403 #[test]
404 fn test_duplicate_host_case_insensitive() {
405 let d = SmugglingDetector::new();
407 let headers = vec![
408 ("HOST".to_string(), "a.com".to_string()),
409 ("host".to_string(), "a.com".to_string()),
410 ];
411 let r = d.detect(&headers);
412 assert_eq!(r.unwrap_err().0, SmugglingKind::DuplicateHost);
413 }
414
415 #[test]
416 fn test_valid_te_chunked() {
417 let d = SmugglingDetector::new();
418 let headers = vec![
419 ("Host".to_string(), "example.com".to_string()),
420 ("Transfer-Encoding".to_string(), "gzip, chunked".to_string()),
421 ];
422 assert!(d.detect(&headers).is_ok());
423 }
424
425 #[test]
426 fn test_invalid_control_chars() {
427 let d = SmugglingDetector::new();
428 let headers = vec![
429 ("Host".to_string(), "example.com".to_string()),
430 ("X".to_string(), "val\x00ue".to_string()),
431 ];
432 let r = d.detect(&headers);
433 assert_eq!(r.unwrap_err().0, SmugglingKind::HeaderInjection);
434 }
435
436 #[test]
437 fn test_smuggling_kind_all_variants() {
438 let kinds = [
439 SmugglingKind::ClTe,
440 SmugglingKind::TeCl,
441 SmugglingKind::TeTe,
442 SmugglingKind::HostInjection,
443 SmugglingKind::HeaderInjection,
444 SmugglingKind::DoubleContentLength,
445 ];
446 for k in kinds.iter() {
447 let s = k.as_str();
448 assert!(!s.is_empty());
449 }
450 }
451
452 #[test]
453 fn test_smuggling_kind_as_str() {
454 assert_eq!(SmugglingKind::ClTe.as_str(), "CL.TE");
455 assert_eq!(SmugglingKind::TeCl.as_str(), "TE.CL");
456 assert_eq!(SmugglingKind::TeTe.as_str(), "TE.TE");
457 assert_eq!(SmugglingKind::HostInjection.as_str(), "Host Injection");
458 assert_eq!(SmugglingKind::HeaderInjection.as_str(), "Header Injection");
459 assert_eq!(SmugglingKind::DoubleContentLength.as_str(), "Double Content-Length");
460 }
461
462 #[test]
463 fn test_smuggling_detector_default() {
464 let d = SmugglingDetector;
465 let headers = vec![("Host".to_string(), "example.com".to_string())];
466 assert!(d.detect(&headers).is_ok());
467 }
468
469 #[test]
470 fn test_smuggling_detector_new() {
471 let d = SmugglingDetector::new();
472 let headers = vec![("Host".to_string(), "example.com".to_string())];
473 assert!(d.detect(&headers).is_ok());
474 }
475
476 #[test]
477 fn test_detect_err_wraps_correctly() {
478 let d = SmugglingDetector::new();
479 let headers = vec![
480 ("Host".to_string(), "example.com".to_string()),
481 ("Content-Length".to_string(), "10".to_string()),
482 ("Content-Length".to_string(), "20".to_string()),
483 ];
484 let r = d.detect_err(&headers);
485 assert!(r.is_err());
486 let err = r.unwrap_err();
487 assert!(matches!(err, Http1Error::SmugglingDetected(_)));
488 let err_str = err.to_string();
489 assert!(err_str.contains("Double Content-Length"));
490 }
491
492 #[test]
493 fn test_content_length_not_decimal() {
494 let d = SmugglingDetector::new();
495 let headers = vec![
496 ("Host".to_string(), "example.com".to_string()),
497 ("Content-Length".to_string(), "12a3".to_string()),
498 ];
499 let r = d.detect(&headers);
500 assert!(r.is_err());
501 assert_eq!(r.unwrap_err().0, SmugglingKind::ClTe);
502 }
503
504 #[test]
505 fn test_content_length_negative_rejected() {
506 let d = SmugglingDetector::new();
507 let headers = vec![
508 ("Host".to_string(), "example.com".to_string()),
509 ("Content-Length".to_string(), "-5".to_string()),
510 ];
511 let r = d.detect(&headers);
512 assert!(r.is_err());
513 }
514
515 #[test]
516 fn test_transfer_encoding_whitespace() {
517 let d = SmugglingDetector::new();
518 let headers = vec![
519 ("Host".to_string(), "example.com".to_string()),
520 ("Transfer-Encoding".to_string(), " chunked ".to_string()),
521 ];
522 assert!(d.detect(&headers).is_ok());
523 }
524
525 #[test]
526 fn test_transfer_encoding_mixed_case() {
527 let d = SmugglingDetector::new();
528 let headers = vec![
529 ("Host".to_string(), "example.com".to_string()),
530 ("Transfer-Encoding".to_string(), "Chunked".to_string()),
531 ];
532 let r = d.detect(&headers);
533 assert!(r.is_ok());
534 }
535
536 #[test]
537 fn test_header_name_case_insensitive() {
538 let d = SmugglingDetector::new();
539 let headers = vec![
540 ("HOST".to_string(), "example.com".to_string()),
541 ("content-length".to_string(), "10".to_string()),
542 ];
543 assert!(d.detect(&headers).is_ok());
544 }
545
546 #[test]
547 fn test_null_byte_in_header_value() {
548 let d = SmugglingDetector::new();
549 let headers = vec![
550 ("Host".to_string(), "example.com".to_string()),
551 ("X-Test".to_string(), "val\x00ue".to_string()),
552 ];
553 let r = d.detect(&headers);
554 assert!(r.is_err());
555 assert_eq!(r.unwrap_err().0, SmugglingKind::HeaderInjection);
556 }
557
558 #[test]
559 fn test_vtab_in_header_value() {
560 let d = SmugglingDetector::new();
561 let headers = vec![
562 ("Host".to_string(), "example.com".to_string()),
563 ("X-Test".to_string(), "val\x0bue".to_string()),
564 ];
565 let r = d.detect(&headers);
566 assert!(r.is_err());
567 assert_eq!(r.unwrap_err().0, SmugglingKind::HeaderInjection);
568 }
569
570 #[test]
571 fn test_formfeed_in_header_value() {
572 let d = SmugglingDetector::new();
573 let headers = vec![
574 ("Host".to_string(), "example.com".to_string()),
575 ("X-Test".to_string(), "val\x0cue".to_string()),
576 ];
577 let r = d.detect(&headers);
578 assert!(r.is_err());
579 assert_eq!(r.unwrap_err().0, SmugglingKind::HeaderInjection);
580 }
581
582 #[test]
583 fn test_cr_only_in_header_value() {
584 let d = SmugglingDetector::new();
585 let headers = vec![
586 ("Host".to_string(), "example.com".to_string()),
587 ("X-Test".to_string(), "val\revil".to_string()),
588 ];
589 let r = d.detect(&headers);
590 assert!(r.is_err());
591 assert_eq!(r.unwrap_err().0, SmugglingKind::HeaderInjection);
592 }
593
594 #[test]
595 fn test_lf_only_in_header_value() {
596 let d = SmugglingDetector::new();
597 let headers = vec![
598 ("Host".to_string(), "example.com".to_string()),
599 ("X-Test".to_string(), "val\nevil".to_string()),
600 ];
601 let r = d.detect(&headers);
602 assert!(r.is_err());
603 assert_eq!(r.unwrap_err().0, SmugglingKind::HeaderInjection);
604 }
605
606 #[test]
607 fn test_te_cl_direction_distinguished() {
608 let d = SmugglingDetector::new();
610 let headers = vec![
611 ("Host".to_string(), "example.com".to_string()),
612 ("Transfer-Encoding".to_string(), "chunked".to_string()),
613 ("Content-Length".to_string(), "0".to_string()),
614 ];
615 let r = d.detect(&headers);
616 assert!(r.is_err());
617 assert_eq!(
618 r.unwrap_err().0,
619 SmugglingKind::TeCl,
620 "TE 在前必须判定为 TE.CL"
621 );
622 }
623
624 #[test]
625 fn test_cl_te_direction_distinguished() {
626 let d = SmugglingDetector::new();
628 let headers = vec![
629 ("Host".to_string(), "example.com".to_string()),
630 ("Content-Length".to_string(), "0".to_string()),
631 ("Transfer-Encoding".to_string(), "chunked".to_string()),
632 ];
633 let r = d.detect(&headers);
634 assert!(r.is_err());
635 assert_eq!(
636 r.unwrap_err().0,
637 SmugglingKind::ClTe,
638 "CL 在前必须判定为 CL.TE"
639 );
640 }
641
642 #[test]
643 fn test_te_without_chunked_rejected() {
644 let d = SmugglingDetector::new();
646 let headers = vec![
647 ("Host".to_string(), "example.com".to_string()),
648 ("Transfer-Encoding".to_string(), "gzip".to_string()),
649 ];
650 let r = d.detect(&headers);
651 assert_eq!(r.unwrap_err().0, SmugglingKind::TeTe);
652 }
653
654 #[test]
655 fn test_te_identity_rejected() {
656 let d = SmugglingDetector::new();
658 let headers = vec![
659 ("Host".to_string(), "example.com".to_string()),
660 ("Transfer-Encoding".to_string(), "identity".to_string()),
661 ];
662 let r = d.detect(&headers);
663 assert_eq!(
664 r.unwrap_err().0,
665 SmugglingKind::TeTe,
666 "TE: identity 必须按 TeTe 拒绝(fail-closed)"
667 );
668 }
669
670 #[test]
671 fn test_single_transfer_encoding_chunked() {
672 let d = SmugglingDetector::new();
673 let headers = vec![
674 ("Host".to_string(), "example.com".to_string()),
675 ("Transfer-Encoding".to_string(), "chunked".to_string()),
676 ];
677 assert!(d.detect(&headers).is_ok());
678 }
679
680 #[test]
681 fn test_empty_headers() {
682 let d = SmugglingDetector::new();
683 let headers: Vec<(String, String)> = vec![];
684 assert!(d.detect(&headers).is_ok());
685 }
686
687 #[test]
688 fn test_smuggling_kind_debug() {
689 let k = SmugglingKind::ClTe;
690 let s = format!("{:?}", k);
691 assert!(!s.is_empty());
692 }
693
694 #[test]
695 fn test_smuggling_kind_clone() {
696 let k = SmugglingKind::ClTe;
697 let k2 = k;
698 assert_eq!(k, k2);
699 }
700}