mirror of https://github.com/synctv-org/synctv
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
238 lines
7.9 KiB
Rust
238 lines
7.9 KiB
Rust
//! CORS (Cross-Origin Resource Sharing) tests for the synctv-proxy crate.
|
|
//!
|
|
//! These tests verify that CORS headers are properly restricted based on
|
|
//! an allowed origins list, rather than using a wildcard (*).
|
|
|
|
#![allow(clippy::unwrap_used)]
|
|
use std::sync::Arc;
|
|
|
|
use axum::http::StatusCode;
|
|
|
|
// CORS preflight with allowed origins tests
|
|
|
|
/// Test that Origin in allowed list returns correct CORS headers
|
|
#[test]
|
|
fn test_cors_origin_in_allowed_list_returns_headers() {
|
|
let allowed_origins = vec![
|
|
"https://example.com".to_string(),
|
|
"https://app.example.com".to_string(),
|
|
];
|
|
let cors_config = Arc::new(synctv_proxy::CorsConfig::new(allowed_origins));
|
|
|
|
let response =
|
|
synctv_proxy::proxy_options_preflight_with_cors(Some("https://example.com"), &cors_config);
|
|
|
|
assert_eq!(response.status(), StatusCode::NO_CONTENT);
|
|
|
|
let headers = response.headers();
|
|
assert_eq!(
|
|
headers
|
|
.get("Access-Control-Allow-Origin")
|
|
.map(|v| v.to_str().unwrap()),
|
|
Some("https://example.com"),
|
|
"Origin in allowed list should be echoed back"
|
|
);
|
|
assert!(
|
|
headers.get("Access-Control-Allow-Credentials").is_none(),
|
|
"Proxy preflight should match the main CORS policy and avoid credentialed browser requests by default"
|
|
);
|
|
assert!(
|
|
headers.get("Access-Control-Allow-Methods").is_some(),
|
|
"Should include Allow-Methods"
|
|
);
|
|
assert_eq!(
|
|
headers
|
|
.get("Access-Control-Allow-Methods")
|
|
.map(|v| v.to_str().unwrap()),
|
|
Some("GET, HEAD, OPTIONS"),
|
|
"Proxy preflight must advertise GET, HEAD, and OPTIONS"
|
|
);
|
|
}
|
|
|
|
/// Test that Origin NOT in allowed list is rejected
|
|
#[test]
|
|
fn test_cors_origin_not_in_allowed_list_rejected() {
|
|
let allowed_origins = vec![
|
|
"https://example.com".to_string(),
|
|
"https://app.example.com".to_string(),
|
|
];
|
|
let cors_config = Arc::new(synctv_proxy::CorsConfig::new(allowed_origins));
|
|
|
|
let response =
|
|
synctv_proxy::proxy_options_preflight_with_cors(Some("https://evil.com"), &cors_config);
|
|
|
|
// Should return 403 Forbidden for disallowed origins
|
|
assert_eq!(
|
|
response.status(),
|
|
StatusCode::FORBIDDEN,
|
|
"Origin not in allowed list should be rejected"
|
|
);
|
|
|
|
// Should NOT include CORS headers that would allow the request
|
|
let headers = response.headers();
|
|
assert!(
|
|
headers.get("Access-Control-Allow-Origin").is_none()
|
|
|| headers
|
|
.get("Access-Control-Allow-Origin")
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
!= "https://evil.com",
|
|
"Should not return the evil origin in Allow-Origin"
|
|
);
|
|
}
|
|
|
|
/// Test that empty allowed origins list has safe default behavior
|
|
#[test]
|
|
fn test_cors_empty_allowed_origins_default_behavior() {
|
|
// Empty allowed origins should reject all origins (secure by default)
|
|
let allowed_origins: Vec<String> = vec![];
|
|
let cors_config = Arc::new(synctv_proxy::CorsConfig::new(allowed_origins));
|
|
|
|
let response =
|
|
synctv_proxy::proxy_options_preflight_with_cors(Some("https://any-site.com"), &cors_config);
|
|
|
|
// Should reject when no origins are allowed
|
|
assert_eq!(
|
|
response.status(),
|
|
StatusCode::FORBIDDEN,
|
|
"Empty allowed origins should reject all requests"
|
|
);
|
|
}
|
|
|
|
/// Test missing Origin header behavior
|
|
#[test]
|
|
fn test_cors_missing_origin_header() {
|
|
let allowed_origins = vec!["https://example.com".to_string()];
|
|
let cors_config = Arc::new(synctv_proxy::CorsConfig::new(allowed_origins));
|
|
|
|
// When Origin is missing (non-browser request), behavior depends on policy
|
|
// A secure default is to reject or return minimal headers
|
|
let response = synctv_proxy::proxy_options_preflight_with_cors(None, &cors_config);
|
|
|
|
// Missing origin should still return a valid response
|
|
// but without Access-Control-Allow-Origin header
|
|
assert!(
|
|
response.status() == StatusCode::NO_CONTENT || response.status() == StatusCode::FORBIDDEN,
|
|
"Missing origin should return a valid status"
|
|
);
|
|
}
|
|
|
|
/// Test wildcard (*) is not allowed when using explicit origins
|
|
#[test]
|
|
fn test_cors_wildcard_not_echoed() {
|
|
let allowed_origins = vec!["https://example.com".to_string()];
|
|
let cors_config = Arc::new(synctv_proxy::CorsConfig::new(allowed_origins));
|
|
|
|
// Request with "*" as Origin should NOT be treated specially
|
|
let response = synctv_proxy::proxy_options_preflight_with_cors(Some("*"), &cors_config);
|
|
|
|
// "*" is not in the allowed list, so should be rejected
|
|
assert_eq!(
|
|
response.status(),
|
|
StatusCode::FORBIDDEN,
|
|
"Wildcard should not bypass origin check"
|
|
);
|
|
}
|
|
|
|
/// Test Vary header is set correctly for caching
|
|
#[test]
|
|
fn test_cors_vary_header_set() {
|
|
let allowed_origins = vec!["https://example.com".to_string()];
|
|
let cors_config = Arc::new(synctv_proxy::CorsConfig::new(allowed_origins));
|
|
|
|
let response =
|
|
synctv_proxy::proxy_options_preflight_with_cors(Some("https://example.com"), &cors_config);
|
|
|
|
let headers = response.headers();
|
|
|
|
// Vary: Origin is important for caching - prevents serving cached
|
|
// CORS responses for different origins
|
|
let vary = headers.get("Vary").map(|v| v.to_str().unwrap());
|
|
assert!(
|
|
vary.is_some() && vary.unwrap().contains("Origin"),
|
|
"Should include Vary: Origin header for proper caching"
|
|
);
|
|
}
|
|
|
|
/// Test multiple allowed origins
|
|
#[test]
|
|
fn test_cors_multiple_allowed_origins() {
|
|
let allowed_origins = vec![
|
|
"https://example.com".to_string(),
|
|
"https://app.example.com".to_string(),
|
|
"https://cdn.example.com".to_string(),
|
|
];
|
|
let cors_config = Arc::new(synctv_proxy::CorsConfig::new(allowed_origins));
|
|
|
|
// Test each allowed origin
|
|
for origin in &[
|
|
"https://example.com",
|
|
"https://app.example.com",
|
|
"https://cdn.example.com",
|
|
] {
|
|
let response = synctv_proxy::proxy_options_preflight_with_cors(Some(*origin), &cors_config);
|
|
|
|
assert_eq!(
|
|
response.status(),
|
|
StatusCode::NO_CONTENT,
|
|
"Origin {origin} should be allowed"
|
|
);
|
|
|
|
let headers = response.headers();
|
|
assert_eq!(
|
|
headers
|
|
.get("Access-Control-Allow-Origin")
|
|
.map(|v| v.to_str().unwrap()),
|
|
Some(*origin),
|
|
"Origin {origin} should be echoed back"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Test CORS config with wildcard enabled (special mode)
|
|
#[test]
|
|
fn test_cors_wildcard_mode_allows_all() {
|
|
// When configured with wildcard mode, all origins are allowed
|
|
let cors_config = Arc::new(synctv_proxy::CorsConfig::new_wildcard());
|
|
|
|
let response = synctv_proxy::proxy_options_preflight_with_cors(
|
|
Some("https://any-random-site.com"),
|
|
&cors_config,
|
|
);
|
|
|
|
assert_eq!(
|
|
response.status(),
|
|
StatusCode::NO_CONTENT,
|
|
"Wildcard mode should allow all origins"
|
|
);
|
|
|
|
// In wildcard mode, we return * but this is explicitly configured,
|
|
// not the default behavior
|
|
let headers = response.headers();
|
|
assert!(
|
|
headers.get("Access-Control-Allow-Origin").is_some(),
|
|
"Wildcard mode should return Allow-Origin header"
|
|
);
|
|
}
|
|
|
|
// Security: Wildcard mode with credentials is forbidden
|
|
|
|
/// Test that wildcard mode does NOT include credentials header
|
|
/// Per CORS spec, Access-Control-Allow-Credentials cannot be used with wildcard origin
|
|
#[test]
|
|
fn test_wildcard_mode_no_credentials() {
|
|
let cors_config = Arc::new(synctv_proxy::CorsConfig::new_wildcard());
|
|
|
|
let response =
|
|
synctv_proxy::proxy_options_preflight_with_cors(Some("https://any-site.com"), &cors_config);
|
|
|
|
let headers = response.headers();
|
|
|
|
// Wildcard mode must NOT include Allow-Credentials
|
|
assert!(
|
|
headers.get("Access-Control-Allow-Credentials").is_none(),
|
|
"Wildcard mode must NOT include Access-Control-Allow-Credentials (per CORS spec)"
|
|
);
|
|
}
|