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
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
2️⃣ What is CSRF?
CSRF stands for Cross-Site Request Forgery.
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.
⚠️ 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
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
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
6️⃣ *Step 1: Add UUID Dependency
*
In Cargo.toml:
uuid = { version = "1", features = ["v4"] }
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
}),
}))
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
}
});
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,
}))
}
🔟 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
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 })
}
}
1️⃣3️⃣ Register CSRF Middleware in main.rs
.wrap(CsrfProtection)
Make sure order is correct:
.wrap(cors)
.wrap(SessionMiddleware::builder(...))
.wrap(RequireLogin)
.wrap(CsrfProtection)
1️⃣4️⃣ Why Middleware Order Matters
Correct order:
CORS
Session
Login middleware
CSRF middleware
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
1️⃣7️⃣ Real Production Flow
Login → Session created
↓
CSRF token generated
↓
Frontend stores token
↓
POST request with header
↓
Middleware validates token
↓
Success
Top comments (0)