Debug School

rakesh kumar
rakesh kumar

Posted on

CSRF Protection in Actix-Web (Rust) – Complete Production Guide for Session-Based Applications

Introduction
What is CSRF?
Why Session-Based Systems Are Vulnerable
When Do You Need CSRF Protection?
CSRF Protection Strategy
Step 1: Add UUID Dependency
Step 2: Generate CSRF Token During Login
Step 3: Frontend Sends CSRF Token
Step 4: Validate CSRF in Backend Handler
Why Compare Session Token With Header?
Which Routes Should Require CSRF?
Cleaner Approach: CSRF Middleware
Register CSRF Middleware in main.rs
Why Middleware Order Matters
Common CSRF Mistakes
Production Security Best Practices
Real Production Flow

Introduction

If your application uses session-based authentication, it is automatically vulnerable to a common web attack called CSRF (Cross-Site Request Forgery) — unless you protect it properly.

Most developers focus on login security but forget CSRF protection. In production systems, this can allow attackers to:

Submit unwanted forms

Change passwords

Delete data

Perform transactions

Modify records
Enter fullscreen mode Exit fullscreen mode

In this guide, we will implement a secure CSRF protection system in Actix-Web using:

Session-based tokens

Header validation

Middleware approach (optional advanced)

Production-ready best practices
Enter fullscreen mode Exit fullscreen mode

2️⃣ What is CSRF?

CSRF stands for Cross-Site Request Forgery.
Enter fullscreen mode Exit fullscreen mode

It happens when:

A user is logged into your system.

They visit a malicious website.

That website sends a hidden request to your backend.

Browser automatically includes session cookies.

Backend processes the request — thinking it came from the real user.
Enter fullscreen mode Exit fullscreen mode

⚠️ The user never intended to perform that action.

3️⃣ Why Session-Based Systems Are Vulnerable

Because:

Browser automatically sends cookies

Session ID is stored in cookies

Backend trusts session
Enter fullscreen mode Exit fullscreen mode

Without CSRF validation, attackers can abuse this.

4️⃣ When Do You Need CSRF Protection?

You MUST implement CSRF if:

You use session cookies

You have POST, PUT, DELETE routes

Your frontend and backend are separate

You run admin dashboards
Enter fullscreen mode Exit fullscreen mode

If you use JWT in Authorization header, CSRF risk is lower.

5️⃣ CSRF Protection Strategy

We will implement:


Generate CSRF token at login

Store token in session

Send token to frontend

Frontend sends token in header

Backend validates token for every state-changing request
Enter fullscreen mode Exit fullscreen mode

6️⃣ *Step 1: Add UUID Dependency
*

In Cargo.toml:

uuid = { version = "1", features = ["v4"] }
Enter fullscreen mode Exit fullscreen mode

7️⃣ Step 2: Generate CSRF Token During Login

In login handler:

use uuid::Uuid;

let csrf_token = Uuid::new_v4().to_string();

// Store in session
session.insert("csrf_token", csrf_token.clone())?;

// Return in response
Ok(HttpResponse::Ok().json(ApiResponse {
    message: "Login successful".to_string(),
    data: json!({
        "username": user.username,
        "role": user.role,
        "csrf_token": csrf_token
    }),
}))
Enter fullscreen mode Exit fullscreen mode

Now frontend receives CSRF token after login.

8️⃣ Step 3: Frontend Sends CSRF Token

Frontend must include token in header:

axios.post("/api/shops", data, {
  headers: {
    "X-CSRF-TOKEN": csrfToken
  }
});
Enter fullscreen mode Exit fullscreen mode

We use custom header:

X-CSRF-TOKEN
9️⃣ Step 4: Validate CSRF in Backend Handler

Example: Protect POST route

use actix_web::HttpRequest;

pub async fn create_shop(
    req: HttpRequest,
    session: Session,
    body: web::Json<ShopForm>,
) -> Result<HttpResponse> {

    let expected_token = session
        .get::<String>("csrf_token")?
        .unwrap_or_default();

    let provided_token = req
        .headers()
        .get("X-CSRF-TOKEN")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("");

    if expected_token != provided_token {
        return Err(error::ErrorForbidden("Invalid CSRF token"));
    }

    Ok(HttpResponse::Ok().json(ApiResponse {
        message: "Shop created".to_string(),
        data: true,
    }))
}
Enter fullscreen mode Exit fullscreen mode

🔟 Why Compare Session Token With Header?

Because:

Attacker site cannot read your session token

Attacker cannot guess random UUID

Only your frontend has valid token

This blocks CSRF attacks.

1️⃣1️⃣ Which Routes Should Require CSRF?

Protect:

POST

PUT

PATCH

DELETE
Enter fullscreen mode Exit fullscreen mode

Do NOT require CSRF for:

GET

Public endpoints

1️⃣2️⃣Cleaner Approach: CSRF Middleware

Instead of validating inside every handler, create middleware.

Create CSRF Middleware

File:

src/middleware/csrf.rs
use actix_service::{Service, Transform};
use actix_web::{dev::{ServiceRequest, ServiceResponse}, Error, error};
use futures_util::future::{ready, LocalBoxFuture, Ready};
use actix_session::SessionExt;

pub struct CsrfProtection;

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

    fn new_transform(&self, service: S) -> Self::Future {
        ready(Ok(CsrfMiddleware { service }))
    }
}

pub struct CsrfMiddleware<S> {
    service: S,
}

impl<S, B> Service<ServiceRequest> for CsrfMiddleware<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 method = req.method().clone();

        if method == "POST" || method == "PUT" || method == "DELETE" {
            let session = req.get_session();

            let expected = session.get::<String>("csrf_token").ok().flatten().unwrap_or_default();

            let provided = req.headers()
                .get("X-CSRF-TOKEN")
                .and_then(|v| v.to_str().ok())
                .unwrap_or("")
                .to_string();

            if expected.is_empty() || expected != provided {
                return Box::pin(async {
                    Err(error::ErrorForbidden("Invalid CSRF token"))
                });
            }
        }

        let fut = self.service.call(req);
        Box::pin(async move { fut.await })
    }
}
Enter fullscreen mode Exit fullscreen mode

1️⃣3️⃣ Register CSRF Middleware in main.rs

.wrap(CsrfProtection)
Enter fullscreen mode Exit fullscreen mode

Make sure order is correct:

.wrap(cors)
.wrap(SessionMiddleware::builder(...))
.wrap(RequireLogin)
.wrap(CsrfProtection)
Enter fullscreen mode Exit fullscreen mode

1️⃣4️⃣ Why Middleware Order Matters

Correct order:

CORS

Session

Login middleware

CSRF middleware
Enter fullscreen mode Exit fullscreen mode

Session must be available before CSRF validation.

1️⃣5️⃣ Common CSRF Mistakes

❌ Not generating token at login
❌ Not returning token to frontend
❌ Protecting GET requests unnecessarily
❌ Using predictable token values
❌ Forgetting to enable credentials in CORS

1️⃣6️⃣ Production Security Best Practices


Use HTTPS

Set .cookie_secure(true)

Set SameSite=Lax or Strict

Use random UUID tokens

Combine with session timeout
Enter fullscreen mode Exit fullscreen mode

1️⃣7️⃣ Real Production Flow

Login → Session created
        ↓
CSRF token generated
        ↓
Frontend stores token
        ↓
POST request with header
        ↓
Middleware validates token
        ↓
Success
Enter fullscreen mode Exit fullscreen mode

CSRF vs JWT Systems

Top comments (0)