Salvo CSRF Protection
[dependencies]
salvo = { version = "0.89.3", features = ["csrf"] }Ciphers
| Function prefix | Key type | Notes |
|---|---|---|
bcrypt_* | — | No key. Slowest (bcrypt hashing). |
hmac_* | [u8; 32] | Fast. |
aes_gcm_* | [u8; 32] | Authenticated encryption. |
ccp_* | [u8; 32] | ChaCha20Poly1305. |
Each cipher pairs with a store: *_cookie_csrf(...) or *_session_csrf(...). Session variants require a SessionHandler installed earlier in the chain.
Basic: Cookie Store + Form Token
use salvo::csrf::{bcrypt_cookie_csrf, CsrfDepotExt, FormFinder};
use salvo::prelude::*;
use serde::Deserialize;
#[derive(Deserialize)]
struct FormData {
csrf_token: String,
message: String,
}
#[handler]
async fn show_form(depot: &mut Depot, res: &mut Response) {
let token = depot.csrf_token().unwrap_or_default();
res.render(Text::Html(format!(
r#"<form method="post">
<input type="hidden" name="csrf_token" value="{token}" />
<input type="text" name="message" />
<button type="submit">Submit</button>
</form>"#
)));
}
#[handler]
async fn handle_form(req: &mut Request, res: &mut Response) {
let data = req.parse_form::<FormData>().await.unwrap();
res.render(format!("Message received: {}", data.message));
}
#[tokio::main]
async fn main() {
let csrf = bcrypt_cookie_csrf(FormFinder::new("csrf_token"));
let router = Router::new().hoop(csrf).get(show_form).post(handle_form);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}Cipher Variants
use salvo::csrf::{
bcrypt_cookie_csrf, hmac_cookie_csrf, aes_gcm_cookie_csrf, ccp_cookie_csrf,
bcrypt_session_csrf, hmac_session_csrf, aes_gcm_session_csrf, ccp_session_csrf,
FormFinder,
};
let finder = FormFinder::new("csrf_token");
let key = *b"01234567012345670123456701234567"; // 32 bytes
let _ = bcrypt_cookie_csrf(finder.clone());
let _ = hmac_cookie_csrf(key, finder.clone());
let _ = aes_gcm_cookie_csrf(key, finder.clone());
let _ = ccp_cookie_csrf(key, finder.clone());Session Store
Session-backed CSRF needs a SessionHandler hooped before the CSRF handler:
use salvo::csrf::{bcrypt_session_csrf, FormFinder};
use salvo::session::{CookieStore, SessionHandler};
use salvo::prelude::*;
#[tokio::main]
async fn main() {
let session = SessionHandler::builder(
CookieStore::new(),
b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
).build().unwrap();
let csrf = bcrypt_session_csrf(FormFinder::new("csrf_token"));
let router = Router::new()
.hoop(session) // must come first
.hoop(csrf)
.get(show_form)
.post(handle_form);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}Token Finders
use salvo::csrf::{FormFinder, HeaderFinder, JsonFinder};
FormFinder::new("csrf_token"); // application/x-www-form-urlencoded body
HeaderFinder::new("x-csrf-token"); // HTTP header
JsonFinder::new("csrf_token"); // JSON body fieldOnly FormFinder, HeaderFinder, and JsonFinder exist in salvo::csrf. There is no QueryFinder. Use Csrf::add_finder to accept the token from multiple locations.
Retrieving the Token
use salvo::csrf::CsrfDepotExt;
#[handler]
async fn show_form(depot: &mut Depot, res: &mut Response) {
let token = depot.csrf_token().unwrap_or_default();
res.render(Text::Html(format!(
r#"<form method="post"><input type="hidden" name="csrf_token" value="{token}" /></form>"#
)));
}A new token is generated on every request; re-render it each time.
Scoped CSRF Middleware
let form_finder = FormFinder::new("csrf_token");
let bcrypt_csrf = bcrypt_cookie_csrf(form_finder.clone());
let hmac_csrf = hmac_cookie_csrf(*b"01234567012345670123456701234567", form_finder);
let router = Router::new()
.push(Router::with_hoop(bcrypt_csrf).path("forms").get(show_form).post(handle_form))
.push(Router::with_hoop(hmac_csrf).path("api").get(get_token).post(api_handler));AJAX / Header-based
use salvo::csrf::{HeaderFinder, hmac_cookie_csrf};
let csrf = hmac_cookie_csrf(
*b"01234567012345670123456701234567",
HeaderFinder::new("x-csrf-token"),
);Client:
fetch('/api', {
method: 'POST',
headers: { 'X-CSRF-Token': token },
body: JSON.stringify(data),
});Salvo-specific Notes
- CSRF middleware only checks "unsafe" methods (POST/PUT/PATCH/DELETE); GET/HEAD/OPTIONS bypass validation and refresh the token.
- Session-backed variants require
SessionHandlerhooped before the CSRF handler, otherwise the store cannot locate a session. - When pairing with
salvo-cors, CSRF headers (e.g.x-csrf-token) must be listed inallow_headers.
Related Skills
- salvo-cors: CORS configuration for cross-origin requests
- salvo-session: CSRF with session-based storage