1use crate::shadow::ShadowMirrorConfig;
7use crate::trap::TrapConfig;
8use crate::vhost::SiteConfig;
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use std::collections::HashSet;
12use std::fmt;
13use std::fs;
14use std::path::Path;
15use tracing::{debug, info, warn};
16
17const MAX_CONFIG_SIZE: u64 = 10 * 1024 * 1024;
19
20#[derive(Clone, Serialize, Deserialize, JsonSchema)]
22pub struct GlobalConfig {
23 #[serde(default = "default_http_addr")]
25 pub http_addr: String,
26 #[serde(default = "default_https_addr")]
28 pub https_addr: String,
29 #[serde(default)]
31 pub workers: usize,
32 #[serde(default = "default_shutdown_timeout")]
34 pub shutdown_timeout_secs: u64,
35 #[serde(default = "default_waf_threshold")]
37 pub waf_threshold: u8,
38 #[serde(default = "default_true")]
40 pub waf_enabled: bool,
41 #[serde(default = "default_log_level")]
43 pub log_level: String,
44 #[serde(default)]
46 pub admin_api_key: Option<String>,
47 #[serde(default)]
49 pub trap_config: Option<TrapConfig>,
50 #[serde(default = "default_waf_regex_timeout_ms")]
53 pub waf_regex_timeout_ms: u64,
54}
55
56fn default_waf_regex_timeout_ms() -> u64 {
57 100 }
59
60fn default_http_addr() -> String {
61 "0.0.0.0:80".to_string()
62}
63
64fn default_https_addr() -> String {
65 "0.0.0.0:443".to_string()
66}
67
68fn default_shutdown_timeout() -> u64 {
69 30
70}
71
72fn default_waf_threshold() -> u8 {
73 70
74}
75
76fn default_true() -> bool {
77 true
78}
79
80fn default_log_level() -> String {
81 "info".to_string()
82}
83
84impl fmt::Debug for GlobalConfig {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 f.debug_struct("GlobalConfig")
87 .field("http_addr", &self.http_addr)
88 .field("https_addr", &self.https_addr)
89 .field("workers", &self.workers)
90 .field("shutdown_timeout_secs", &self.shutdown_timeout_secs)
91 .field("waf_threshold", &self.waf_threshold)
92 .field("waf_enabled", &self.waf_enabled)
93 .field("log_level", &self.log_level)
94 .field(
95 "admin_api_key",
96 &self.admin_api_key.as_ref().map(|_| "[REDACTED]"),
97 )
98 .field("trap_config", &self.trap_config)
99 .field("waf_regex_timeout_ms", &self.waf_regex_timeout_ms)
100 .finish()
101 }
102}
103
104impl Default for GlobalConfig {
105 fn default() -> Self {
106 Self {
107 http_addr: default_http_addr(),
108 https_addr: default_https_addr(),
109 workers: 0,
110 shutdown_timeout_secs: default_shutdown_timeout(),
111 waf_threshold: default_waf_threshold(),
112 waf_enabled: true,
113 log_level: default_log_level(),
114 admin_api_key: None,
115 trap_config: None,
116 waf_regex_timeout_ms: default_waf_regex_timeout_ms(),
117 }
118 }
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
123pub struct RateLimitConfig {
124 pub rps: u32,
126 #[serde(default = "default_true")]
128 pub enabled: bool,
129 pub burst: Option<u32>,
131}
132
133impl Default for RateLimitConfig {
134 fn default() -> Self {
135 Self {
136 rps: 10000,
137 enabled: true,
138 burst: None,
139 }
140 }
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
145pub struct UpstreamConfig {
146 pub host: String,
148 pub port: u16,
150 #[serde(default = "default_weight")]
152 pub weight: u32,
153 #[serde(skip)]
155 pub healthy: bool,
156}
157
158fn default_weight() -> u32 {
159 1
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
164pub struct TlsConfig {
165 pub cert_path: String,
167 pub key_path: String,
169 #[serde(default = "default_min_tls")]
171 pub min_version: String,
172}
173
174fn default_min_tls() -> String {
175 "1.2".to_string()
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
180pub struct AccessControlConfig {
181 #[serde(default)]
183 pub allow: Vec<String>,
184 #[serde(default)]
186 pub deny: Vec<String>,
187 #[serde(default)]
189 pub default_action: String,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
194pub struct HeaderConfig {
195 #[serde(default)]
197 pub request: HeaderOps,
198 #[serde(default)]
200 pub response: HeaderOps,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
205pub struct HeaderOps {
206 #[serde(default)]
208 pub add: std::collections::HashMap<String, String>,
209 #[serde(default)]
211 pub set: std::collections::HashMap<String, String>,
212 #[serde(default)]
214 pub remove: Vec<String>,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
219pub struct SiteWafConfig {
220 #[serde(default = "default_true")]
222 pub enabled: bool,
223 pub threshold: Option<u8>,
225 #[serde(default)]
227 pub rule_overrides: std::collections::HashMap<String, String>,
228}
229
230impl Default for SiteWafConfig {
231 fn default() -> Self {
232 Self {
233 enabled: true,
234 threshold: None,
235 rule_overrides: std::collections::HashMap::new(),
236 }
237 }
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
242pub struct SiteYamlConfig {
243 pub hostname: String,
245 pub upstreams: Vec<UpstreamConfig>,
247 pub tls: Option<TlsConfig>,
249 pub waf: Option<SiteWafConfig>,
251 pub rate_limit: Option<RateLimitConfig>,
253 pub access_control: Option<AccessControlConfig>,
255 pub headers: Option<HeaderConfig>,
257 #[serde(default)]
259 pub shadow_mirror: Option<ShadowMirrorConfig>,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
264pub struct ProfilerConfig {
265 #[serde(default = "default_true")]
267 pub enabled: bool,
268 #[serde(default = "default_max_profiles")]
270 pub max_profiles: usize,
271 #[serde(default = "default_max_schemas")]
273 pub max_schemas: usize,
274 #[serde(default = "default_min_samples")]
276 pub min_samples_for_validation: u32,
277
278 #[serde(default = "default_payload_z_threshold")]
283 pub payload_z_threshold: f64,
284
285 #[serde(default = "default_param_z_threshold")]
287 pub param_z_threshold: f64,
288
289 #[serde(default = "default_response_z_threshold")]
291 pub response_z_threshold: f64,
292
293 #[serde(default = "default_min_stddev")]
295 pub min_stddev: f64,
296
297 #[serde(default = "default_type_ratio_threshold")]
300 pub type_ratio_threshold: f64,
301
302 #[serde(default = "default_max_type_counts")]
307 pub max_type_counts: usize,
308
309 #[serde(default = "default_true")]
311 pub redact_pii: bool,
312
313 #[serde(default)]
316 pub freeze_after_samples: u32,
317}
318
319fn default_max_profiles() -> usize {
320 1000
321}
322
323fn default_max_schemas() -> usize {
324 500
325}
326
327fn default_min_samples() -> u32 {
328 100
329}
330
331fn default_payload_z_threshold() -> f64 {
332 3.0
333}
334
335fn default_param_z_threshold() -> f64 {
336 4.0
337}
338
339fn default_response_z_threshold() -> f64 {
340 4.0
341}
342
343fn default_min_stddev() -> f64 {
344 0.01
345}
346
347fn default_type_ratio_threshold() -> f64 {
348 0.9
349}
350
351fn default_max_type_counts() -> usize {
352 10
353}
354
355impl Default for ProfilerConfig {
356 fn default() -> Self {
357 Self {
358 enabled: true,
359 max_profiles: default_max_profiles(),
360 max_schemas: default_max_schemas(),
361 min_samples_for_validation: default_min_samples(),
362 payload_z_threshold: default_payload_z_threshold(),
363 param_z_threshold: default_param_z_threshold(),
364 response_z_threshold: default_response_z_threshold(),
365 min_stddev: default_min_stddev(),
366 type_ratio_threshold: default_type_ratio_threshold(),
367 max_type_counts: default_max_type_counts(),
368 redact_pii: true,
369 freeze_after_samples: 0,
370 }
371 }
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
376pub struct ConfigFile {
377 #[serde(default)]
379 pub server: GlobalConfig,
380 pub sites: Vec<SiteYamlConfig>,
382 #[serde(default)]
384 pub rate_limit: RateLimitConfig,
385 #[serde(default)]
387 pub profiler: ProfilerConfig,
388}
389
390#[derive(Debug, thiserror::Error)]
392pub enum ConfigError {
393 #[error("configuration file not found: {path} (check the path or mount the file into the container)")]
394 NotFound { path: String },
395
396 #[error("configuration file too large: {size} bytes (max {max} bytes). Reduce size or split the configuration")]
397 FileTooLarge { size: u64, max: u64 },
398
399 #[error("failed to read configuration: {0}")]
400 IoError(#[from] std::io::Error),
401
402 #[error("failed to parse configuration: {0}")]
403 ParseError(#[from] serde_yaml::Error),
404
405 #[error("validation error: {0}")]
406 ValidationError(String),
407
408 #[error("TLS certificate not found: {path} (set tls.cert_path to a valid PEM file)")]
409 CertNotFound { path: String },
410
411 #[error("TLS key not found: {path} (set tls.key_path to a valid PEM file)")]
412 KeyNotFound { path: String },
413
414 #[error("duplicate hostname: {hostname} (hostnames must be unique; consider a wildcard like '*.example.com')")]
415 DuplicateHostname { hostname: String },
416
417 #[error("invalid TLS version: {version} (set min_version to '1.2' or '1.3')")]
418 InvalidTlsVersion { version: String },
419
420 #[error("path traversal detected in: {path} (remove '..' or encoded traversal sequences)")]
421 PathTraversal { path: String },
422}
423
424fn contains_path_traversal(path: &str) -> bool {
432 if path.contains("..") {
434 return true;
435 }
436
437 let path_lower = path.to_lowercase();
439 if path_lower.contains("%2e%2e") || path_lower.contains("%2e.") || path_lower.contains(".%2e") {
440 return true;
441 }
442
443 if path_lower.contains("%252e") {
445 return true;
446 }
447
448 if path.contains('\0') || path_lower.contains("%00") {
450 return true;
451 }
452
453 false
454}
455
456pub struct ConfigLoader;
458
459impl ConfigLoader {
460 pub fn load<P: AsRef<Path>>(path: P) -> Result<ConfigFile, ConfigError> {
468 let path = path.as_ref();
469 info!("Loading configuration from: {}", path.display());
470
471 if !path.exists() {
473 return Err(ConfigError::NotFound {
474 path: path.display().to_string(),
475 });
476 }
477
478 let metadata = fs::metadata(path)?;
480 if metadata.len() > MAX_CONFIG_SIZE {
481 return Err(ConfigError::FileTooLarge {
482 size: metadata.len(),
483 max: MAX_CONFIG_SIZE,
484 });
485 }
486
487 let contents = fs::read_to_string(path)?;
489 let config: ConfigFile = serde_yaml::from_str(&contents)?;
490
491 Self::validate(&config)?;
493
494 info!("Loaded configuration with {} sites", config.sites.len());
495 Ok(config)
496 }
497
498 fn validate(config: &ConfigFile) -> Result<(), ConfigError> {
500 let mut hostnames = HashSet::new();
501
502 for site in &config.sites {
503 let normalized = site.hostname.to_lowercase();
505 if !hostnames.insert(normalized.clone()) {
506 return Err(ConfigError::DuplicateHostname {
507 hostname: site.hostname.clone(),
508 });
509 }
510
511 if site.upstreams.is_empty() {
513 return Err(ConfigError::ValidationError(format!(
514 "site '{}' has no upstreams configured; add at least one upstream with host and port",
515 site.hostname
516 )));
517 }
518
519 if let Some(tls) = &site.tls {
521 Self::validate_tls(tls)?;
522 }
523
524 if let Some(waf) = &site.waf {
526 if !waf.enabled {
528 warn!(
529 site = %site.hostname,
530 "WAF protection DISABLED for site - backend may be exposed to attacks"
531 );
532 }
533 if let Some(threshold) = waf.threshold {
535 if threshold == 0 {
536 return Err(ConfigError::ValidationError(format!(
537 "site '{}' has WAF threshold of 0, which effectively disables protection. \
538 Use waf.enabled: false to disable the WAF, or set threshold between 1-100",
539 site.hostname
540 )));
541 }
542 if threshold > 100 {
543 return Err(ConfigError::ValidationError(format!(
544 "site '{}' has invalid WAF threshold {} (must be 1-100); \
545 use waf.enabled: false to disable or set a valid threshold",
546 site.hostname, threshold
547 )));
548 }
549 }
550 }
551
552 if let Some(rl) = &site.rate_limit {
554 if rl.rps == 0 && rl.enabled {
555 warn!(
556 "Site '{}' has rate limiting enabled with 0 RPS; set rps > 0 or disable rate limiting",
557 site.hostname
558 );
559 }
560 if rl.rps > 1_000_000 {
562 return Err(ConfigError::ValidationError(format!(
563 "site '{}' has extreme RPS limit {} (max 1,000,000)",
564 site.hostname, rl.rps
565 )));
566 }
567 }
568
569 if let Some(shadow) = &site.shadow_mirror {
571 if let Err(e) = shadow.validate() {
572 return Err(ConfigError::ValidationError(format!(
573 "site '{}' has invalid shadow_mirror config: {}. Fix shadow_mirror settings or remove the block",
574 site.hostname,
575 e
576 )));
577 }
578 }
579 }
580
581 if config.server.workers > 1024 {
583 return Err(ConfigError::ValidationError(format!(
584 "extreme worker count {} (max 1024)",
585 config.server.workers
586 )));
587 }
588
589 if config.server.shutdown_timeout_secs > 3600 {
590 return Err(ConfigError::ValidationError(format!(
591 "extreme shutdown timeout {}s (max 3600)",
592 config.server.shutdown_timeout_secs
593 )));
594 }
595
596 if config.server.waf_regex_timeout_ms > 500 {
597 return Err(ConfigError::ValidationError(format!(
598 "extreme WAF regex timeout {}ms (max 500)",
599 config.server.waf_regex_timeout_ms
600 )));
601 }
602
603 if !config.server.waf_enabled {
605 warn!(
606 "Global WAF protection DISABLED - all sites may be exposed to attacks unless individually configured"
607 );
608 }
609
610 if config.server.waf_threshold == 0 {
612 return Err(ConfigError::ValidationError(
613 "global WAF threshold of 0 effectively disables protection. \
614 Use waf_enabled: false to disable globally, or set waf_threshold between 1-100"
615 .to_string(),
616 ));
617 }
618 if config.server.waf_threshold > 100 {
619 return Err(ConfigError::ValidationError(format!(
620 "global WAF threshold {} is invalid (must be 1-100); set waf_threshold between 1-100",
621 config.server.waf_threshold
622 )));
623 }
624
625 Ok(())
626 }
627
628 fn validate_tls(tls: &TlsConfig) -> Result<(), ConfigError> {
630 if contains_path_traversal(&tls.cert_path) {
632 return Err(ConfigError::PathTraversal {
633 path: tls.cert_path.clone(),
634 });
635 }
636 if contains_path_traversal(&tls.key_path) {
637 return Err(ConfigError::PathTraversal {
638 path: tls.key_path.clone(),
639 });
640 }
641
642 if !Path::new(&tls.cert_path).exists() {
644 return Err(ConfigError::CertNotFound {
645 path: tls.cert_path.clone(),
646 });
647 }
648
649 if !Path::new(&tls.key_path).exists() {
651 return Err(ConfigError::KeyNotFound {
652 path: tls.key_path.clone(),
653 });
654 }
655
656 match tls.min_version.as_str() {
658 "1.2" | "1.3" => {}
659 _ => {
660 return Err(ConfigError::InvalidTlsVersion {
661 version: tls.min_version.clone(),
662 });
663 }
664 }
665
666 debug!(
667 "Validated TLS config: cert={}, key=[REDACTED]",
668 tls.cert_path
669 );
670 Ok(())
671 }
672
673 pub fn to_site_configs(config: &ConfigFile) -> Vec<SiteConfig> {
675 config
676 .sites
677 .iter()
678 .map(|site| SiteConfig {
679 hostname: site.hostname.clone(),
680 upstreams: site
681 .upstreams
682 .iter()
683 .map(|u| format!("{}:{}", u.host, u.port))
684 .collect(),
685 tls_enabled: site.tls.is_some(),
686 tls_cert: site.tls.as_ref().map(|t| t.cert_path.clone()),
687 tls_key: site.tls.as_ref().map(|t| t.key_path.clone()),
688 waf_threshold: site.waf.as_ref().and_then(|w| w.threshold),
689 waf_enabled: site.waf.as_ref().map(|w| w.enabled).unwrap_or(true),
690 access_control: site.access_control.clone(),
691 headers: site.headers.as_ref().map(|headers| headers.compile()),
692 shadow_mirror: site.shadow_mirror.clone(),
693 })
694 .collect()
695 }
696}
697
698#[cfg(test)]
699mod tests {
700 use super::*;
701 use std::io::Write;
702 use tempfile::NamedTempFile;
703
704 fn create_temp_config(content: &str) -> NamedTempFile {
705 let mut file = NamedTempFile::new().unwrap();
706 file.write_all(content.as_bytes()).unwrap();
707 file
708 }
709
710 #[test]
711 fn test_load_minimal_config() {
712 let yaml = r#"
713sites:
714 - hostname: example.com
715 upstreams:
716 - host: 127.0.0.1
717 port: 8080
718"#;
719 let file = create_temp_config(yaml);
720 let config = ConfigLoader::load(file.path()).unwrap();
721 assert_eq!(config.sites.len(), 1);
722 assert_eq!(config.sites[0].hostname, "example.com");
723 }
724
725 #[test]
726 fn test_load_full_config() {
727 let yaml = r#"
728server:
729 http_addr: "0.0.0.0:8080"
730 https_addr: "0.0.0.0:8443"
731 workers: 4
732 waf_threshold: 80
733 log_level: debug
734
735rate_limit:
736 rps: 5000
737 enabled: true
738
739sites:
740 - hostname: example.com
741 upstreams:
742 - host: 127.0.0.1
743 port: 8080
744 weight: 2
745 waf:
746 enabled: true
747 threshold: 60
748"#;
749 let file = create_temp_config(yaml);
750 let config = ConfigLoader::load(file.path()).unwrap();
751
752 assert_eq!(config.server.http_addr, "0.0.0.0:8080");
753 assert_eq!(config.server.workers, 4);
754 assert_eq!(config.rate_limit.rps, 5000);
755 assert_eq!(config.sites[0].waf.as_ref().unwrap().threshold, Some(60));
756 }
757
758 #[test]
759 fn test_duplicate_hostname() {
760 let yaml = r#"
761sites:
762 - hostname: example.com
763 upstreams:
764 - host: 127.0.0.1
765 port: 8080
766 - hostname: example.com
767 upstreams:
768 - host: 127.0.0.1
769 port: 8081
770"#;
771 let file = create_temp_config(yaml);
772 let result = ConfigLoader::load(file.path());
773 assert!(matches!(result, Err(ConfigError::DuplicateHostname { .. })));
774 }
775
776 #[test]
777 fn test_no_upstreams() {
778 let yaml = r#"
779sites:
780 - hostname: example.com
781 upstreams: []
782"#;
783 let file = create_temp_config(yaml);
784 let result = ConfigLoader::load(file.path());
785 assert!(matches!(result, Err(ConfigError::ValidationError(_))));
786 }
787
788 #[test]
789 fn test_invalid_waf_threshold() {
790 let yaml = r#"
791sites:
792 - hostname: example.com
793 upstreams:
794 - host: 127.0.0.1
795 port: 8080
796 waf:
797 threshold: 150
798"#;
799 let file = create_temp_config(yaml);
800 let result = ConfigLoader::load(file.path());
801 assert!(matches!(result, Err(ConfigError::ValidationError(_))));
802 }
803
804 #[test]
805 fn test_file_not_found() {
806 let result = ConfigLoader::load("/nonexistent/config.yaml");
807 assert!(matches!(result, Err(ConfigError::NotFound { .. })));
808 }
809
810 #[test]
811 fn test_default_values() {
812 let config = GlobalConfig::default();
813 assert_eq!(config.http_addr, "0.0.0.0:80");
814 assert_eq!(config.https_addr, "0.0.0.0:443");
815 assert_eq!(config.waf_threshold, 70);
816 assert!(config.waf_enabled);
817 assert_eq!(config.waf_regex_timeout_ms, 100); }
819
820 #[test]
821 fn test_debug_redacts_admin_api_key() {
822 let mut config = GlobalConfig::default();
823 config.admin_api_key = Some("super-secret-key-12345".to_string());
824
825 let debug_output = format!("{:?}", config);
826
827 assert!(!debug_output.contains("super-secret-key-12345"));
829 assert!(debug_output.contains("[REDACTED]"));
831 assert!(debug_output.contains("0.0.0.0:80"));
833 }
834
835 #[test]
836 fn test_debug_shows_none_when_no_key() {
837 let config = GlobalConfig::default();
838 let debug_output = format!("{:?}", config);
839
840 assert!(debug_output.contains("None"));
842 assert!(!debug_output.contains("[REDACTED]"));
843 }
844
845 #[test]
846 fn test_waf_regex_timeout_config() {
847 let yaml = r#"
848server:
849 waf_regex_timeout_ms: 200
850sites:
851 - hostname: example.com
852 upstreams:
853 - host: 127.0.0.1
854 port: 8080
855"#;
856 let file = create_temp_config(yaml);
857 let config = ConfigLoader::load(file.path()).unwrap();
858 assert_eq!(config.server.waf_regex_timeout_ms, 200);
859 }
860
861 #[test]
862 fn test_waf_regex_timeout_default() {
863 let yaml = r#"
864sites:
865 - hostname: example.com
866 upstreams:
867 - host: 127.0.0.1
868 port: 8080
869"#;
870 let file = create_temp_config(yaml);
871 let config = ConfigLoader::load(file.path()).unwrap();
872 assert_eq!(config.server.waf_regex_timeout_ms, 100);
874 }
875
876 #[test]
877 fn test_to_site_configs() {
878 let yaml = r#"
879sites:
880 - hostname: example.com
881 upstreams:
882 - host: 127.0.0.1
883 port: 8080
884 waf:
885 enabled: true
886 threshold: 80
887"#;
888 let file = create_temp_config(yaml);
889 let config = ConfigLoader::load(file.path()).unwrap();
890 let sites = ConfigLoader::to_site_configs(&config);
891
892 assert_eq!(sites.len(), 1);
893 assert_eq!(sites[0].hostname, "example.com");
894 assert_eq!(sites[0].waf_threshold, Some(80));
895 assert!(sites[0].waf_enabled);
896 }
897
898 #[test]
899 fn test_yaml_with_unknown_fields_passes() {
900 let yaml = r#"
904server:
905 http_addr: "0.0.0.0:9090"
906 unknown_field: "should be ignored"
907 another_mystery: 42
908sites:
909 - hostname: example.com
910 upstreams:
911 - host: 127.0.0.1
912 port: 8080
913 extra_site_field: true
914"#;
915 let file = create_temp_config(yaml);
916 let config = ConfigLoader::load(file.path()).unwrap();
917 assert_eq!(config.server.http_addr, "0.0.0.0:9090");
918 assert_eq!(config.sites.len(), 1);
919 assert_eq!(config.sites[0].hostname, "example.com");
920 }
921
922 #[test]
923 fn test_yaml_with_unknown_top_level_field_passes() {
924 let yaml = r#"
926some_future_feature:
927 enabled: true
928sites:
929 - hostname: example.com
930 upstreams:
931 - host: 127.0.0.1
932 port: 8080
933"#;
934 let file = create_temp_config(yaml);
935 let config = ConfigLoader::load(file.path()).unwrap();
936 assert_eq!(config.sites.len(), 1);
937 }
938
939 #[test]
940 fn test_path_traversal_detection() {
941 use super::contains_path_traversal;
942
943 assert!(contains_path_traversal(".."));
945 assert!(contains_path_traversal("../etc/passwd"));
946 assert!(contains_path_traversal("/path/../secret"));
947 assert!(contains_path_traversal("path/to/../../root"));
948
949 assert!(contains_path_traversal("%2e%2e"));
951 assert!(contains_path_traversal("%2E%2E"));
952 assert!(contains_path_traversal("%2e."));
953 assert!(contains_path_traversal(".%2e"));
954
955 assert!(contains_path_traversal("%252e%252e"));
957 assert!(contains_path_traversal("path/%252e%252e/file"));
958
959 assert!(contains_path_traversal("\x00"));
961 assert!(contains_path_traversal("path\x00/file"));
962 assert!(contains_path_traversal("%00"));
963 assert!(contains_path_traversal("path/%00/file"));
964
965 assert!(!contains_path_traversal("/path/to/file"));
967 assert!(!contains_path_traversal("certs/server.pem"));
968 assert!(!contains_path_traversal("/etc/nginx/ssl/cert.pem"));
969 assert!(!contains_path_traversal("./relative/path")); }
971}