Debug School

rakesh kumar
rakesh kumar

Posted on

Security Headers Middleware in Actix-Web (Rust)

Production-ready theory + step-by-step code flow (main.rs → middleware → handler → db)

Security headers are HTTP response headers that tell browsers how to behave. They reduce common attacks like XSS, clickjacking, MIME sniffing, and data leakage. In a Rust/Actix backend, the clean way is to apply these headers using middleware, so every response gets the same secure defaults without repeating code in each handler.

In this blog, you will build a custom Security Headers middleware and wire it into main.rs. You’ll also see the request flow: middleware runs, handler executes, DB query runs, then middleware adds headers to the response.

1) What are security headers?
What

Security headers are response headers that enforce browser security rules.

Why

They reduce risk from:

Cross-site scripting (XSS)

Clickjacking (UI redress attacks)

MIME-type sniffing (wrong content treated as executable)

Referrer leakage

Insecure resource loading
Enter fullscreen mode Exit fullscreen mode

When

Use security headers on:

All API responses (safe defaults)

Any server that serves HTML pages or assets

Admin dashboards and user-facing sites
Enter fullscreen mode Exit fullscreen mode

Even if you return JSON only, these headers are still beneficial.

2) Request flow (middleware → handler → DB → middleware)

A secure request looks like this:

Client calls GET /api/shops

Middleware receives request

Middleware forwards request to handler

Handler checks session and calls DB

Handler returns JSON response

Middleware adds security headers to response

Response goes back to client

This ensures every response is protected automatically.

3) Step 1: Decide which headers to set

Below is a good production default:

✅ X-Content-Type-Options: nosniff
Prevents MIME sniffing.

✅ X-Frame-Options: DENY
Blocks clickjacking (no iframe embedding).

✅ Referrer-Policy: no-referrer (or strict-origin-when-cross-origin)
Reduces referrer leakage.

✅ Permissions-Policy: ...
Disables browser features you don’t use (camera, mic, etc).

✅ Strict-Transport-Security (HSTS)
Force HTTPS, but only in production with real TLS.

✅ Content-Security-Policy (CSP)
Powerful header for XSS control (more relevant when serving HTML). For pure JSON APIs you can still set a safe CSP, but it matters most when you return pages.

4) Step 2: Create Security Headers middleware

Create file:

src/middleware/security_headers.rs

use actix_service::{Service, Transform};
use actix_web::{
    dev::{ServiceRequest, ServiceResponse},
    http::header::{HeaderName, HeaderValue},
    Error,
};
use futures_util::future::{ready, LocalBoxFuture, Ready};

#[derive(Clone)]
pub struct SecurityHeaders {
    // enable HSTS only in production
    pub enable_hsts: bool,
    // optional CSP if you serve HTML
    pub csp: Option<String>,
}

impl SecurityHeaders {
    pub fn new(enable_hsts: bool, csp: Option<String>) -> Self {
        Self { enable_hsts, csp }
    }
}

impl<S, B> Transform<S, ServiceRequest> for SecurityHeaders
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Transform = SecurityHeadersMiddleware<S>;
    type InitError = ();
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ready(Ok(SecurityHeadersMiddleware {
            service,
            cfg: self.clone(),
        }))
    }
}

pub struct SecurityHeadersMiddleware<S> {
    service: S,
    cfg: SecurityHeaders,
}

impl<S, B> Service<ServiceRequest> for SecurityHeadersMiddleware<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

    fn call(&self, req: ServiceRequest) -> Self::Future {
        let cfg = self.cfg.clone();
        let fut = self.service.call(req);

        Box::pin(async move {
            let mut res = fut.await?;

            // 1) No MIME sniffing
            res.headers_mut().insert(
                HeaderName::from_static("x-content-type-options"),
                HeaderValue::from_static("nosniff"),
            );

            // 2) Block iframe embedding (clickjacking)
            res.headers_mut().insert(
                HeaderName::from_static("x-frame-options"),
                HeaderValue::from_static("deny"),
            );

            // 3) Reduce referrer leakage
            res.headers_mut().insert(
                HeaderName::from_static("referrer-policy"),
                HeaderValue::from_static("strict-origin-when-cross-origin"),
            );

            // 4) Browser features control
            res.headers_mut().insert(
                HeaderName::from_static("permissions-policy"),
                HeaderValue::from_static("camera=(), microphone=(), geolocation=(), payment=()"),
            );

            // 5) HSTS only for production HTTPS
            if cfg.enable_hsts {
                res.headers_mut().insert(
                    HeaderName::from_static("strict-transport-security"),
                    HeaderValue::from_static("max-age=31536000; includeSubDomains"),
                );
            }

            // 6) Optional CSP (mainly for HTML pages)
            if let Some(csp) = cfg.csp {
                if let Ok(v) = HeaderValue::from_str(&csp) {
                    res.headers_mut().insert(
                        HeaderName::from_static("content-security-policy"),
                        v,
                    );
                }
            }

            Ok(res)
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

2–3 simple lines about what this does

This middleware runs after your handler returns a response. It then injects security headers into every response automatically. This means you don’t need to add headers in each handler.

5) Step 3: Register middleware module

Create:


src/middleware/mod.rs
Enter fullscreen mode Exit fullscreen mode

pub mod security_headers;
6) S*tep 4: Wire it into main.rs*

In main.rs:

mod middleware;
use middleware::security_headers::SecurityHeaders;
Enter fullscreen mode Exit fullscreen mode

Enable HSTS only in production:

let is_production = std::env::var("APP_ENV").unwrap_or_default() == "production";

// If you serve only JSON, CSP can be optional.
// If you serve HTML templates, set a real CSP.
Enter fullscreen mode Exit fullscreen mode

let csp = Some("default-src 'self'; frame-ancestors 'none'; base-uri 'self'".to_string());


HttpServer::new(move || {
    App::new()
        .app_data(state.clone())
        .wrap(cors)
        .wrap(
            SessionMiddleware::builder(CookieSessionStore::default(), key.clone())
                .cookie_secure(is_production)
                .cookie_same_site(SameSite::Lax)
                .build(),
        )
        // ✅ Security headers middleware
        .wrap(SecurityHeaders::new(is_production, csp.clone()))
        .service(
            web::scope("/api")
                .route("/shops", web::get().to(handlers::list_shops_handler))
                .route("/login", web::post().to(handlers::login))
        )
        .service(Files::new("/uploads", "./uploads"))
})
.bind(cfg.bind_addr)?
.run()
.await
Enter fullscreen mode Exit fullscreen mode

Why .wrap(SecurityHeaders...) is placed here

Because it should apply to all responses under the app. Any handler that returns JSON will automatically include these security headers.

7) Step 5: Handler example that hits DB
handlers/shop.rs

use actix_session::Session;
use actix_web::{error, web, HttpResponse, Result};

use crate::db::{list_shops_by_vendor, AppState};
use super::common::{ensure_login_user_id, ApiResponse};

pub async fn list_shops_handler(
    state: web::Data<AppState>,
    session: Session,
) -> Result<HttpResponse> {
    // Auth check
    let user_id = ensure_login_user_id(&session)?;

    let vendor_id = i32::try_from(user_id).map_err(|_| error::ErrorBadRequest("User id invalid"))?;

    // DB call
    let shops = list_shops_by_vendor(&state.pool, vendor_id)
        .await
        .map_err(error::ErrorInternalServerError)?;

    // Response
    Ok(HttpResponse::Ok().json(ApiResponse {
        message: "Shops fetched".to_string(),
        data: shops,
    }))
}
Enter fullscreen mode Exit fullscreen mode

DB function (example)

db/shop.rs

use sqlx::MySqlPool;

pub async fn list_shops_by_vendor(pool: &MySqlPool, vendor_id: i32) -> Result<Vec<Shop>, sqlx::Error> {
    let rows = sqlx::query_as::<_, Shop>("SELECT * FROM shops WHERE vendor_id = ?")
        .bind(vendor_id)
        .fetch_all(pool)
        .await?;

    Ok(rows)
}
Enter fullscreen mode Exit fullscreen mode

8) Confirm headers in browser/network

Call:


curl -i http://127.0.0.1:8080/api/shops
Enter fullscreen mode Exit fullscreen mode

You should see headers like:

X-Content-Type-Options: nosniff
X-Frame-Options: deny
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Strict-Transport-Security: max-age=31536000; includeSubDomains   (only production)
Content-Security-Policy: default-src 'self'; frame-ancestors 'none'; base-uri 'self'
Enter fullscreen mode Exit fullscreen mode

9) Production tips (important)
Use HSTS only when HTTPS is guaranteed

If you enable HSTS on a domain that sometimes serves HTTP, users may get locked out.

Be careful with CSP

If you serve frontend HTML, CSP needs to allow required scripts/styles. For API-only backend, CSP is optional.

Add Cache-Control: no-store for auth endpoints

For login/session sensitive endpoints:

res.headers_mut().insert(
    HeaderName::from_static("cache-control"),
    HeaderValue::from_static("no-store"),
);
Enter fullscreen mode Exit fullscreen mode

Top comments (0)