Private
Public Access
1
0

fix: remove dead MtlsMiddleware, add security header middleware, document rustls as auth gate (closes #13)
Some checks failed
CI/CD Pipeline / Code Format (push) Successful in 3s
CI/CD Pipeline / Clippy Lints (push) Successful in 42s
CI/CD Pipeline / All Unit Tests (push) Successful in 1m11s
CI/CD Pipeline / Security Audit (push) Successful in 5s
CI/CD Pipeline / Enrollment Tests (push) Successful in 1m13s
CI/CD Pipeline / Verify Enrollment CLI Flag (push) Successful in 58s
CI/CD Pipeline / Build Debian Package (Ubuntu 22.04) (push) Failing after 8s
CI/CD Pipeline / Build Debian Package (push) Failing after 5s
CI/CD Pipeline / Build RPM Package (push) Successful in 2m5s
CI/CD Pipeline / Build Arch Package (push) Successful in 2m16s
CI/CD Pipeline / Build Alpine Package (push) Failing after 3m5s

- Remove dead MtlsMiddleware struct, MtlsMiddlewareService, Transform/Service impls
- Remove validate_client_certificate() stub (returned Ok(()) unconditionally)
- Remove has_duplicate_critical_headers() from mtls.rs (moved to new module)
- Convert build_rustls_config() from method on MtlsMiddleware to free function
- Create SecurityHeadersMiddleware in src/auth/security_headers.rs for VULN-006
- Wire SecurityHeadersMiddleware into Actix-web pipeline in main.rs
- Add ADR documenting rustls as authoritative client-auth gate
- Preserve CrlAwareVerifier, MtlsConfig, MtlsError, ClientCertInfo, build_rustls_config
- Add integration tests for duplicate header detection
- Update HARDENING_REPORT.md and SECURITY_FINDINGS_REPORT.md with ADR

Co-authored-by: git-echo <git-echo@moon-dragon.us>
This commit is contained in:
Draco-Lunaris-Echo
2026-06-06 13:58:01 -05:00
committed by GitHub
parent efaac33c47
commit 6a4c4c95a4
8 changed files with 458 additions and 421 deletions

View File

@ -0,0 +1,166 @@
//! Security Headers Middleware Module
//!
//! Provides request-level security header validation for Actix-web.
//! Enforces VULN-006: rejects requests with duplicate critical headers
//! (content-type, authorization, host) to prevent HTTP request smuggling
//! and response-splitting attacks.
use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
Error,
};
use futures_util::future::LocalBoxFuture;
use tracing::warn;
/// Critical headers that MUST NOT appear more than once in a request.
/// Duplicate values for these headers can enable request smuggling,
/// response splitting, and other HTTP parsing ambiguities.
const CRITICAL_HEADERS: &[&str] = &["content-type", "authorization", "host"];
/// Security headers middleware for Actix-web.
///
/// Checks every incoming request for duplicate critical headers (VULN-006)
/// and rejects malformed requests with HTTP 400 Bad Request.
pub struct SecurityHeadersMiddleware;
impl SecurityHeadersMiddleware {
/// Create a new security headers middleware instance.
pub fn new() -> Self {
Self
}
}
impl Default for SecurityHeadersMiddleware {
fn default() -> Self {
Self::new()
}
}
/// Actix-web Transform implementation — wraps SecurityHeadersMiddleware as middleware
impl<S, B> Transform<S, ServiceRequest> for SecurityHeadersMiddleware
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = SecurityHeadersMiddlewareService<S>;
type Future = futures_util::future::Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
futures_util::future::ok(SecurityHeadersMiddlewareService { service })
}
}
/// Security headers middleware service — performs per-request duplicate header checks
pub struct SecurityHeadersMiddlewareService<S> {
service: S,
}
impl<S, B> Service<ServiceRequest> for SecurityHeadersMiddlewareService<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
// VULN-006: Check for duplicate critical headers before processing
if has_duplicate_critical_headers(req.headers()) {
let peer_addr = req.peer_addr();
warn!(
peer_addr = ?peer_addr,
"Duplicate critical headers detected - rejecting request (VULN-006)"
);
return Box::pin(async move {
Err(actix_web::error::ErrorBadRequest(
"Duplicate critical headers not allowed",
))
});
}
// All checks passed — call the service
let fut = self.service.call(req);
Box::pin(fut)
}
}
/// Check for duplicate critical headers (VULN-006).
/// Returns true if any critical header appears more than once.
///
/// This function is public for testing purposes.
pub fn has_duplicate_critical_headers(headers: &actix_web::http::header::HeaderMap) -> bool {
for header_name in CRITICAL_HEADERS.iter() {
let mut count = 0;
for (name, _value) in headers.iter() {
if name.as_str().eq_ignore_ascii_case(header_name) {
count += 1;
if count > 1 {
return true;
}
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::http::header;
#[test]
fn test_no_duplicate_headers_passes() {
let mut headers = header::HeaderMap::new();
headers.insert(header::CONTENT_TYPE, "application/json".parse().unwrap());
headers.insert(header::AUTHORIZATION, "Bearer test".parse().unwrap());
headers.insert(header::HOST, "localhost".parse().unwrap());
assert!(!has_duplicate_critical_headers(&headers));
}
#[test]
fn test_duplicate_content_type_rejected() {
let mut headers = header::HeaderMap::new();
headers.insert(header::CONTENT_TYPE, "application/json".parse().unwrap());
headers.append(header::CONTENT_TYPE, "text/plain".parse().unwrap());
assert!(has_duplicate_critical_headers(&headers));
}
#[test]
fn test_duplicate_authorization_rejected() {
let mut headers = header::HeaderMap::new();
headers.insert(header::AUTHORIZATION, "Bearer test1".parse().unwrap());
headers.append(header::AUTHORIZATION, "Bearer test2".parse().unwrap());
assert!(has_duplicate_critical_headers(&headers));
}
#[test]
fn test_duplicate_host_rejected() {
let mut headers = header::HeaderMap::new();
headers.insert(header::HOST, "localhost".parse().unwrap());
headers.append(header::HOST, "evil.com".parse().unwrap());
assert!(has_duplicate_critical_headers(&headers));
}
#[test]
fn test_non_critical_duplicate_headers_allowed() {
// Duplicate non-critical headers should be allowed
let mut headers = header::HeaderMap::new();
headers.insert(header::ACCEPT, "text/html".parse().unwrap());
headers.append(header::ACCEPT, "application/json".parse().unwrap());
assert!(!has_duplicate_critical_headers(&headers));
}
#[test]
fn test_empty_headers_passes() {
let headers = header::HeaderMap::new();
assert!(!has_duplicate_critical_headers(&headers));
}
}