CSRF Protection
Cross-Site Request Forgery (CSRF) protection for your RustAPI applications using the Double-Submit Cookie pattern.
What is CSRF?
CSRF is an attack that tricks users into submitting unintended requests. For example, a malicious website could submit a form to your API while users are logged in, performing actions without their consent.
RustAPI’s CSRF protection works by:
- Generating a cryptographic token stored in a cookie
- Requiring the same token in a request header for state-changing requests
- Rejecting requests where the cookie and header don’t match
Quick Start
[dependencies]
rustapi-rs = { version = "0.1.335", features = ["csrf"] }
use rustapi_rs::prelude::*;
use rustapi_extras::csrf::{CsrfConfig, CsrfLayer, CsrfToken};
#[rustapi_rs::get("/form")]
async fn show_form(token: CsrfToken) -> Html<String> {
Html(format!(r#"
<form method="POST" action="/submit">
<input type="hidden" name="csrf_token" value="{}" />
<button type="submit">Submit</button>
</form>
"#, token.as_str()))
}
#[rustapi_rs::post("/submit")]
async fn handle_submit() -> &'static str {
// If we get here, CSRF validation passed!
"Form submitted successfully"
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let csrf_config = CsrfConfig::new()
.cookie_name("csrf_token")
.header_name("X-CSRF-Token");
RustApi::new()
.layer(CsrfLayer::new(csrf_config))
.mount(show_form)
.mount(handle_submit)
.run("127.0.0.1:8080")
.await
}
Configuration Options
#![allow(unused)]
fn main() {
let config = CsrfConfig::new()
// Cookie settings
.cookie_name("csrf_token") // Default: "csrf_token"
.cookie_path("/") // Default: "/"
.cookie_domain("example.com") // Default: None (same domain)
.cookie_secure(true) // Default: true (HTTPS only)
.cookie_http_only(false) // Default: false (JS needs access)
.cookie_same_site(SameSite::Strict) // Default: Strict
// Token settings
.header_name("X-CSRF-Token") // Default: "X-CSRF-Token"
.token_length(32); // Default: 32 bytes
}
How It Works
Safe Methods (No Validation)
GET, HEAD, OPTIONS, and TRACE requests are considered “safe” and don’t modify state. The CSRF middleware:
- ✅ Generates a new token if none exists
- ✅ Sets the token cookie in the response
- ✅ Does NOT validate the header
Unsafe Methods (Validation Required)
POST, PUT, PATCH, and DELETE requests require CSRF validation:
- 🔍 Reads the token from the cookie
- 🔍 Reads the expected token from the header
- ❌ If missing or mismatched → Returns
403 Forbidden - ✅ If valid → Proceeds to handler
Frontend Integration
HTML Forms
For traditional form submissions, include the token as a hidden field:
<form method="POST" action="/api/submit">
<input type="hidden" name="_csrf" value="{{ csrf_token }}" />
<!-- form fields -->
<button type="submit">Submit</button>
</form>
JavaScript / AJAX
For API calls, include the token in the request header:
// Read token from cookie
function getCsrfToken() {
return document.cookie
.split('; ')
.find(row => row.startsWith('csrf_token='))
?.split('=')[1];
}
// Include in fetch requests
fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify({ name: 'John' })
});
Axios Interceptor
import axios from 'axios';
axios.interceptors.request.use(config => {
if (['post', 'put', 'patch', 'delete'].includes(config.method)) {
config.headers['X-CSRF-Token'] = getCsrfToken();
}
return config;
});
Extracting the Token in Handlers
Use the CsrfToken extractor to access the current token in your handlers:
#![allow(unused)]
fn main() {
use rustapi_extras::csrf::CsrfToken;
#[rustapi_rs::get("/api/csrf-token")]
async fn get_csrf_token(token: CsrfToken) -> Json<serde_json::Value> {
Json(serde_json::json!({
"csrf_token": token.as_str()
}))
}
}
Best Practices
1. Always Use HTTPS in Production
#![allow(unused)]
fn main() {
let config = CsrfConfig::new()
.cookie_secure(true); // Cookie only sent over HTTPS
}
2. Use Strict SameSite Policy
#![allow(unused)]
fn main() {
use cookie::SameSite;
let config = CsrfConfig::new()
.cookie_same_site(SameSite::Strict); // Most restrictive
}
3. Combine with Other Security Measures
#![allow(unused)]
fn main() {
RustApi::new()
.layer(CsrfLayer::new(csrf_config))
.layer(SecurityHeadersLayer::strict()) // Add security headers
.layer(CorsLayer::permissive()) // Configure CORS
}
4. Rotate Tokens Periodically
Consider regenerating tokens after sensitive actions:
#![allow(unused)]
fn main() {
#[rustapi_rs::post("/auth/login")]
async fn login(/* ... */) -> impl IntoResponse {
// After successful login, a new CSRF token will be
// generated on the next GET request
// ...
}
}
Testing CSRF Protection
#![allow(unused)]
fn main() {
use rustapi_testing::{TestClient, TestRequest};
#[tokio::test]
async fn test_csrf_protection() {
let app = create_app_with_csrf();
let client = TestClient::new(app);
// GET request should work and set cookie
let res = client.get("/form").await;
assert_eq!(res.status(), StatusCode::OK);
let csrf_cookie = res.headers()
.get("set-cookie")
.unwrap()
.to_str()
.unwrap();
// Extract token value
let token = csrf_cookie
.split(';')
.next()
.unwrap()
.split('=')
.nth(1)
.unwrap();
// POST without token should fail
let res = client.post("/submit").await;
assert_eq!(res.status(), StatusCode::FORBIDDEN);
// POST with correct token should succeed
let res = client.request(
TestRequest::post("/submit")
.header("Cookie", format!("csrf_token={}", token))
.header("X-CSRF-Token", token)
).await;
assert_eq!(res.status(), StatusCode::OK);
}
}
Error Handling
When CSRF validation fails, the middleware returns a JSON error response:
{
"error": {
"code": "csrf_forbidden",
"message": "CSRF token validation failed"
}
}
You can customize this by wrapping the layer with your own error handler.
Security Considerations
| Consideration | Status |
|---|---|
| Token in cookie | ✅ HttpOnly=false (JS needs access) |
| Token validation | ✅ Constant-time comparison |
| SameSite cookie | ✅ Configurable (Strict by default) |
| Secure cookie | ✅ HTTPS-only by default |
| Token entropy | ✅ 32 bytes of cryptographic randomness |
See Also
- JWT Authentication - Token-based authentication
- Security Headers - Additional security layers
- CORS Configuration - Cross-origin request handling