Security
Djangors includes built-in security middleware covering CSRF protection, security response headers, host header validation, and HMAC-signed session cookie storage.
CSRF Protection (CsrfLayer)
CsrfLayer (djangors_core::middleware::csrf_layer()) implements CSRF protection using a double-submit cookie scheme with header and form-body validation fallbacks.
fn main() {
let router_service = djangors_core::router::RouterService::new(djangors_core::Router::new(), false);
use djangors_core::middleware::csrf_layer;
use tower::ServiceBuilder;
let service = ServiceBuilder::new()
.layer(csrf_layer())
.service(router_service);
}
Mechanism & Verification Flow
- Cookie Generation: On safe requests (
GET,HEAD,OPTIONS), if nocsrftokencookie exists, a cryptographically secure random 32-byte hex token is generated and returned in acsrftokencookie (SameSite=Lax; Path=/; Max-Age=31536000). Note: The cookie is intentionally notHttpOnlyso client-side JavaScript can read it to populate request headers. - Unsafe Method Validation: For unsafe HTTP methods (
POST,PUT,PATCH,DELETE), the token in theX-CSRFTokenrequest header is compared against thecsrftokencookie value using constant-time comparison (constant_time_eq). - Form Fallback: If the header is missing or mismatched,
CsrfLayersetsCsrfPendingFormCheckon the request extensions. Form submission handlers verify thecsrfmiddlewaretokenform body field against the pending token, returning403 Forbiddenif missing or invalid.
Production checklist: csrf_layer() defaults to Secure off on the csrftoken cookie, the
same way SignedCookieStore::new() defaults Secure off (see below). This keeps local HTTP
development working out of the box. In production, enable it the same way the session store does:
csrf_layer().with_secure(!settings.debug). Both examples/school and examples/polls wire this
correctly; if you’re copying from an older reference, check that your own main.rs does too. An
internal security review (2026-07-27) found neither example app had actually done this before,
despite the session-cookie guidance below already documenting the correct pattern.
Security Headers (SecurityHeadersLayer)
SecurityHeadersLayer (djangors_core::middleware::security_headers_layer()) sets standard security response headers mimicking Django’s SecurityMiddleware:
fn main() {
let router_service = djangors_core::router::RouterService::new(djangors_core::Router::new(), false);
use tower::ServiceBuilder;
use djangors_core::middleware::security_headers_layer;
let service = ServiceBuilder::new()
.layer(security_headers_layer())
.service(router_service);
}
Headers Set
X-Content-Type-Options: nosniff: Prevents browsers from MIME-sniffing responses away from the declared content type.X-Frame-Options: DENY: Protects against clickjacking by preventing the app from being embedded in<frame>,<iframe>, or<object>elements.Referrer-Policy: same-origin: Restricts referrer information sent in HTTP headers to same-origin requests.
Strict Transport Security (HstsLayer)
Enforces HTTPS via HTTP Strict Transport Security (HSTS):
fn main() {
use djangors_core::middleware::hsts_layer;
// Sets Strict-Transport-Security: max-age=31536000; includeSubDomains
let hsts = hsts_layer(31536000).with_include_subdomains(true);
}
Host Header Validation (HostValidationLayer)
HostValidationLayer validates incoming HTTP Host headers against the project’s ALLOWED_HOSTS setting:
fn main() {
use djangors_core::middleware::HostValidationLayer;
let layer = HostValidationLayer::new(vec!["example.com".to_string(), "api.example.com".to_string()]);
}
- Strip trailing port numbers before matching.
- Requests with disallowed Host headers are immediately rejected with
400 Bad Request: Disallowed Hostbefore reaching application handlers.
Content Security Policy (CspBuilder)
CspBuilder (djangors_core::middleware::CspBuilder) is the django-csp equivalent: a builder
for the Content-Security-Policy response header, with one method per directive:
fn main() {
use djangors_core::middleware::CspBuilder;
let csp_layer = CspBuilder::new()
.default_src(&["'self'"])
.script_src(&["'self'", "https://cdn.example.com"])
.style_src(&["'self'", "'unsafe-inline'"])
.img_src(&["'self'", "data:"])
.frame_ancestors(&["'none'"])
.upgrade_insecure_requests()
.build();
let _ = csp_layer;
}
Supported directives: default_src, script_src, style_src, img_src, connect_src,
font_src, frame_ancestors, form_action, and object_src (each taking a source list), plus
upgrade_insecure_requests() (a bare flag, no source list). Call .report_only(true) before
.build() to send Content-Security-Policy-Report-Only instead of enforcing the policy. This is
useful for testing a new policy against real traffic before turning on enforcement. Directive
values pass through verbatim (no source-keyword validation), matching HstsLayer’s own
deliberately minimal scope, so you’re responsible for getting the CSP syntax right.
Signed Cookie Session Store (SignedCookieStore)
SignedCookieStore (djangors_sessions::SignedCookieStore) stores session data in client-side cookies signed with HMAC-SHA256 using settings.SECRET_KEY:
fn main() {
let secret_key_bytes = b"01234567890123456789012345678901";
use djangors_sessions::{SessionLayer, SignedCookieStore};
let store = SignedCookieStore::new(secret_key_bytes)
.with_cookie_name("djangors_sessionid".into())
.with_secure(true); // Enforce Secure attribute in production
let session_layer = SessionLayer::new(store);
}
Security Features
- HMAC Signature: Cookies are encoded as
b64_json.b64_expiry.b64_mac. Any tampering with payload or expiration invalidates the MAC signature, causingdecode()to returnNone(clearing session state). HttpOnly&SameSite=Lax: Session cookies are markedHttpOnly(inaccessible to client JavaScript) andSameSite=Lax.- Session Fixation Prevention (
cycle_key): Callingsession.cycle_key()during authentication (login()) rotates the session key string, preventing session fixation attacks.
Malware/AV Scanning of Uploads (clamav feature)
djangors-staticfiles’s optional clamav Cargo feature (off by default, so zero cost/deps unless
enabled) scans uploaded file bytes against a real clamd daemon before they’re ever written to
disk or a Storage backend, using clamd’s INSTREAM wire protocol directly.
#![allow(unused)]
fn main() {
use djangors_staticfiles::clamav::{ClamAvScanner, ScanResult};
async fn scan_upload(bytes: &[u8]) -> Result<(), &'static str> {
let scanner = ClamAvScanner::unix("/var/run/clamav/clamd.ctl");
match scanner.scan(bytes).await {
Ok(ScanResult::Clean) => Ok(()),
Ok(ScanResult::Infected(signature)) => {
Err(Box::leak(format!("rejected upload: {signature}").into_boxed_str()))
}
Err(_) => Err("could not reach the malware scanner"),
}
}
}
ClamAvScanner::unix(path) connects over a Unix domain socket (the common case when clamd runs
on the same host); ClamAvScanner::tcp(host, port) connects over TCP instead. Scanning happens
in-memory rather than by handing clamd a file path, since clamd runs as its own OS user and
generally can’t read arbitrary application-owned paths. Scanning in-memory bytes also means
the check can happen before anything touches disk at all. This requires a real clamd daemon
already running and reachable; nothing in this module starts one for you.