// Security Headers Middleware
//
// Adds security headers to protect against common web attacks:
// - Clickjacking (X-Frame-Options, frame-ancestors)
// - MIME sniffing (X-Content-Type-Options)
// - XSS (Content-Security-Policy)
// - Spectre (Cross-Origin-Opener-Policy)
// - Information leakage (Referrer-Policy)
//
// Reference: OWASP Secure Headers Project
// https://owasp.org/www-project-secure-headers/
// ============================================================================
// Security Headers Configuration
// ============================================================================
///|
/// Security headers configuration
pub struct SecurityHeadersConfig {
/// X-Frame-Options value (DENY, SAMEORIGIN)
frame_options : String
/// X-Content-Type-Options (nosniff)
content_type_options : String
/// Referrer-Policy value
referrer_policy : String
/// Cross-Origin-Opener-Policy value
coop : String
/// Cross-Origin-Embedder-Policy value
coep : String
/// Permissions-Policy directives
permissions_policy : String
/// Content-Security-Policy directives
csp : String
/// Cache-Control for HTML pages
cache_control_html : String
/// Cache-Control for static assets
cache_control_static : String
}
///|
/// Default security headers configuration (strict)
pub fn SecurityHeadersConfig::default() -> SecurityHeadersConfig {
{
frame_options: "SAMEORIGIN",
content_type_options: "nosniff",
referrer_policy: "strict-origin-when-cross-origin",
coop: "same-origin",
coep: "require-corp",
permissions_policy: "geolocation=(), microphone=(), camera=()",
csp: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; frame-ancestors 'self'",
cache_control_html: "no-cache, no-store, must-revalidate",
cache_control_static: "public, max-age=31536000, immutable",
}
}
///|
/// Relaxed configuration (for development or when strict CSP breaks things)
pub fn SecurityHeadersConfig::relaxed() -> SecurityHeadersConfig {
{
frame_options: "SAMEORIGIN",
content_type_options: "nosniff",
referrer_policy: "strict-origin-when-cross-origin",
coop: "same-origin-allow-popups",
coep: "unsafe-none",
permissions_policy: "geolocation=(), microphone=(), camera=()",
csp: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:; object-src 'none'; frame-ancestors 'self'",
cache_control_html: "private, no-cache",
cache_control_static: "public, max-age=3600",
}
}
///|
/// Set frame options
pub fn SecurityHeadersConfig::with_frame_options(
self : SecurityHeadersConfig,
value : String,
) -> SecurityHeadersConfig {
{ ..self, frame_options: value }
}
///|
/// Set referrer policy
pub fn SecurityHeadersConfig::with_referrer_policy(
self : SecurityHeadersConfig,
value : String,
) -> SecurityHeadersConfig {
{ ..self, referrer_policy: value }
}
///|
/// Set Cross-Origin-Opener-Policy
pub fn SecurityHeadersConfig::with_coop(
self : SecurityHeadersConfig,
value : String,
) -> SecurityHeadersConfig {
{ ..self, coop: value }
}
///|
/// Set Cross-Origin-Embedder-Policy
pub fn SecurityHeadersConfig::with_coep(
self : SecurityHeadersConfig,
value : String,
) -> SecurityHeadersConfig {
{ ..self, coep: value }
}
///|
/// Set Permissions-Policy
pub fn SecurityHeadersConfig::with_permissions_policy(
self : SecurityHeadersConfig,
value : String,
) -> SecurityHeadersConfig {
{ ..self, permissions_policy: value }
}
///|
/// Set Content-Security-Policy
pub fn SecurityHeadersConfig::with_csp(
self : SecurityHeadersConfig,
value : String,
) -> SecurityHeadersConfig {
{ ..self, csp: value }
}
///|
/// Set Cache-Control for HTML
pub fn SecurityHeadersConfig::with_cache_control_html(
self : SecurityHeadersConfig,
value : String,
) -> SecurityHeadersConfig {
{ ..self, cache_control_html: value }
}
///|
/// Set Cache-Control for static assets
pub fn SecurityHeadersConfig::with_cache_control_static(
self : SecurityHeadersConfig,
value : String,
) -> SecurityHeadersConfig {
{ ..self, cache_control_static: value }
}
// ============================================================================
// Security Headers Middleware
// ============================================================================
///|
/// Create security headers middleware with default configuration
pub fn security_headers() -> Middleware {
security_headers_with_config(SecurityHeadersConfig::default())
}
///|
/// Create security headers middleware with relaxed configuration
pub fn security_headers_relaxed() -> Middleware {
security_headers_with_config(SecurityHeadersConfig::relaxed())
}
///|
/// Create security headers middleware with custom configuration
pub fn security_headers_with_config(
config : SecurityHeadersConfig,
) -> Middleware {
let mars_options = to_mars_secure_headers_options(config)
let @mars.Handler(base_handler) = @mars_mw.secure_headers(
options=mars_options,
)
Middleware(
@mars.Handler(async fn(ctx) {
base_handler(ctx)
// Sol-specific headers not covered by mars secure_headers
if !(config.coop.is_empty()) {
ctx.set_header("Cross-Origin-Opener-Policy", config.coop)
}
if !(config.coep.is_empty()) {
ctx.set_header("Cross-Origin-Embedder-Policy", config.coep)
}
// Add Cache-Control based on content type
let path = ctx.path()
let cache_control = if is_static_asset(path) {
config.cache_control_static
} else {
config.cache_control_html
}
ctx.set_header("Cache-Control", cache_control)
// Continue to next handler
}),
)
}
///|
fn to_mars_secure_headers_options(
config : SecurityHeadersConfig,
) -> @mars_mw.SecureHeadersOptions {
{
hsts: true,
hsts_max_age: 31536000,
hsts_include_subdomains: true,
no_sniff: config.content_type_options == "nosniff",
frame_options: config.frame_options,
xss_protection: true,
csp: config.csp,
referrer_policy: config.referrer_policy,
permissions_policy: config.permissions_policy,
}
}
///|
fn minimal_secure_headers_options() -> @mars_mw.SecureHeadersOptions {
{
hsts: false,
hsts_max_age: 0,
hsts_include_subdomains: false,
no_sniff: false,
frame_options: "",
xss_protection: false,
csp: "",
referrer_policy: "",
permissions_policy: "",
}
}
///|
/// Check if path is a static asset
fn is_static_asset(path : String) -> Bool {
path.has_prefix("/static/") ||
path.has_suffix(".js") ||
path.has_suffix(".css") ||
path.has_suffix(".png") ||
path.has_suffix(".jpg") ||
path.has_suffix(".svg") ||
path.has_suffix(".woff") ||
path.has_suffix(".woff2")
}
// ============================================================================
// Individual Header Middlewares
// ============================================================================
///|
/// Add X-Content-Type-Options: nosniff header
pub fn nosniff() -> Middleware {
let options = { ..minimal_secure_headers_options(), no_sniff: true }
Middleware(@mars_mw.secure_headers(options~))
}
///|
/// Add X-Frame-Options header
pub fn frame_options(value : String) -> Middleware {
let options = { ..minimal_secure_headers_options(), frame_options: value }
Middleware(@mars_mw.secure_headers(options~))
}
///|
/// Add Content-Security-Policy header
pub fn csp(policy : String) -> Middleware {
let options = { ..minimal_secure_headers_options(), csp: policy }
Middleware(@mars_mw.secure_headers(options~))
}
///|
/// Add Referrer-Policy header
pub fn referrer_policy(value : String) -> Middleware {
let options = { ..minimal_secure_headers_options(), referrer_policy: value }
Middleware(@mars_mw.secure_headers(options~))
}
///|
/// Add Cache-Control header
pub fn cache_control(value : String) -> Middleware {
Middleware(
@mars.Handler(async fn(ctx) { ctx.set_header("Cache-Control", value) }),
)
}