#CSRF防御
#什么是CSRF?
CSRF (Cross-Site Request Forgery,跨站请求伪造) 是一种网络安全漏洞,攻击者诱导已认证用户在不知情的情况下执行非本意操作。简单来说,攻击者利用用户已登录的身份,以用户的名义发送恶意请求。
#CSRF攻击原理
攻击步骤通常如下:
- 用户登录了目标网站A并获得了认证Cookie
- 用户访问恶意网站B
- 恶意网站B中的代码自动向网站A发送请求,携带用户的Cookie
- 网站A无法区分这是恶意请求还是用户本人操作
#防护策略
Salvo提供CSRF中间件保护您的应用免受此类攻击:
- 在表单中添加隐藏的CSRF令牌
- 验证用户提交的请求中是否包含有效的CSRF令牌
- 默认验证POST、PATCH、DELETE和PUT请求
#Salvo中的CSRF实现
Csrf 是实现了 Handler 的结构体,内部还有一个 skipper 字段,可以指定跳过某些不需要验证的请求。默认情况下,验证 Method::POST,Method::PATCH,Method::DELETE,Method::PUT 请求。
Salvo支持两种CSRF令牌存储方式:
- CookieStore:将令牌存储在Cookie中,验证请求头或表单中的
csrf-token与Cookie值是否匹配 - SessionStore:将令牌存储在Session中,要求与session中间件一起使用
示例代码 (cookie store)
main.rs
Cargo.toml
use salvo::csrf::*;
use salvo::prelude::*;
use serde::{Deserialize, Serialize};
// Handler for serving the home page with links to different CSRF protection methods
#[handler]
pub async fn home(res: &mut Response) {
let html = r#"
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>Csrf CookieStore</title></head>
<body>
<h2>Csrf Example: CookieStore</h2>
<ul>
<li><a href="/bcrypt/">Bcrypt</a></li>
<li><a href="/hmac/">Hmac</a></li>
<li><a href="/aes_gcm/">Aes Gcm</a></li>
<li><a href="/ccp/">chacha20poly1305</a></li>
</ul>
</body>"#;
res.render(Text::Html(html));
}
// Handler for GET requests that displays a form with CSRF token
#[handler]
pub async fn get_page(depot: &mut Depot, res: &mut Response) {
let new_token = depot.csrf_token().unwrap_or_default();
res.render(Text::Html(get_page_html(new_token, "")));
}
// Handler for POST requests that processes form submission with CSRF validation
#[handler]
pub async fn post_page(req: &mut Request, depot: &mut Depot, res: &mut Response) {
// Define data structure for form submission
#[derive(Deserialize, Serialize, Debug)]
struct Data {
csrf_token: String,
message: String,
}
// Parse the submitted form data into the Data struct
let data = req.parse_form::<Data>().await.unwrap();
// Log the received form data for debugging
tracing::info!("posted data: {:?}", data);
// Generate a new CSRF token for the next request
let new_token = depot.csrf_token().unwrap_or_default();
// Generate HTML response with the new token and display the submitted data
let html = get_page_html(new_token, &format!("{data:#?}"));
// Send the HTML response back to the client
res.render(Text::Html(html));
}
#[tokio::main]
async fn main() {
// Initialize logging system
tracing_subscriber::fmt().init();
// Configure CSRF token finder in form data
let form_finder = FormFinder::new("csrf_token");
// Initialize different CSRF protection methods
let bcrypt_csrf = bcrypt_cookie_csrf(form_finder.clone());
let hmac_csrf = hmac_cookie_csrf(*b"01234567012345670123456701234567", form_finder.clone());
let aes_gcm_cookie_csrf =
aes_gcm_cookie_csrf(*b"01234567012345670123456701234567", form_finder.clone());
let ccp_cookie_csrf =
ccp_cookie_csrf(*b"01234567012345670123456701234567", form_finder.clone());
// Configure router with different CSRF protection endpoints
let router = Router::new()
.get(home)
// Bcrypt-based CSRF protection
.push(
Router::with_hoop(bcrypt_csrf)
.path("bcrypt")
.get(get_page)
.post(post_page),
)
// HMAC-based CSRF protection
.push(
Router::with_hoop(hmac_csrf)
.path("hmac")
.get(get_page)
.post(post_page),
)
// AES-GCM-based CSRF protection
.push(
Router::with_hoop(aes_gcm_cookie_csrf)
.path("aes_gcm")
.get(get_page)
.post(post_page),
)
// ChaCha20Poly1305-based CSRF protection
.push(
Router::with_hoop(ccp_cookie_csrf)
.path("ccp")
.get(get_page)
.post(post_page),
);
// Start server on port 8698
let acceptor = TcpListener::new("0.0.0.0:8698").bind().await;
Server::new(acceptor).serve(router).await;
}
// Helper function to generate HTML page with CSRF token and message
fn get_page_html(csrf_token: &str, msg: &str) -> String {
format!(
r#"
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>Csrf Example</title></head>
<body>
<h2>Csrf Example: CookieStore</h2>
<ul>
<li><a href="/bcrypt/">Bcrypt</a></li>
<li><a href="/hmac/">Hmac</a></li>
<li><a href="/aes_gcm/">Aes Gcm</a></li>
<li><a href="/ccp/">chacha20poly1305</a></li>
</ul>
<form action="./" method="post">
<input type="hidden" name="csrf_token" value="{csrf_token}" />
<div>
<label>Message:<input type="text" name="message" /></label>
</div>
<button type="submit">Send</button>
</form>
<pre>{msg}</pre>
</body>
</html>
"#
)
}
[package]
name = "example-csrf-cookie-store"
version.workspace = true
edition.workspace = true
publish.workspace = true
rust-version.workspace = true
[dependencies]
salvo = { workspace = true, features = ["csrf"] }
tokio = { workspace = true, features = ["macros"] }
tracing.workspace = true
tracing-subscriber.workspace = true
serde = { workspace = true, features = ["derive"] }
示例代码 (session store)
main.rs
Cargo.toml
use salvo::csrf::*;
use salvo::prelude::*;
use serde::{Deserialize, Serialize};
// Handler for serving the home page with links to different CSRF protection methods
#[handler]
pub async fn home(res: &mut Response) {
let html = r#"
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>Csrf SessionStore</title></head>
<body>
<h2>Csrf Example: SessionStore</h2>
<ul>
<li><a href="/bcrypt/">Bcrypt</a></li>
<li><a href="/hmac/">Hmac</a></li>
<li><a href="/aes_gcm/">Aes Gcm</a></li>
<li><a href="/ccp/">chacha20poly1305</a></li>
</ul>
</body>"#;
res.render(Text::Html(html));
}
// Handler for GET requests that displays a form with CSRF token
#[handler]
pub async fn get_page(depot: &mut Depot, res: &mut Response) {
let new_token = depot.csrf_token().unwrap_or_default();
res.render(Text::Html(get_page_html(new_token, "")));
}
// Handler for POST requests that processes form submission with CSRF validation
#[handler]
pub async fn post_page(req: &mut Request, depot: &mut Depot, res: &mut Response) {
// Define data structure for form submission
#[derive(Deserialize, Serialize, Debug)]
struct Data {
csrf_token: String,
message: String,
}
// Parse the submitted form data into the Data struct
let data = req.parse_form::<Data>().await.unwrap();
// Log the received form data for debugging
tracing::info!("posted data: {:?}", data);
// Generate a new CSRF token for the next request
let new_token = depot.csrf_token().unwrap_or_default();
// Generate HTML response with the new token and display the submitted data
let html = get_page_html(new_token, &format!("{data:#?}"));
// Send the HTML response back to the client
res.render(Text::Html(html));
}
#[tokio::main]
async fn main() {
// Initialize logging system
tracing_subscriber::fmt().init();
// Configure CSRF token finder in form data
let form_finder = FormFinder::new("csrf_token");
// Initialize different CSRF protection methods using session store
let bcrypt_csrf = bcrypt_session_csrf(form_finder.clone());
let hmac_csrf = hmac_session_csrf(*b"01234567012345670123456701234567", form_finder.clone());
let aes_gcm_session_csrf =
aes_gcm_session_csrf(*b"01234567012345670123456701234567", form_finder.clone());
let ccp_session_csrf =
ccp_session_csrf(*b"01234567012345670123456701234567", form_finder.clone());
// Configure session handler with memory store and secret key
let session_handler = salvo::session::SessionHandler::builder(
salvo::session::MemoryStore::new(),
b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
)
.build()
.unwrap();
// Configure router with session handler and different CSRF protection endpoints
let router = Router::new()
.get(home)
.hoop(session_handler)
// Bcrypt-based CSRF protection
.push(
Router::with_hoop(bcrypt_csrf)
.path("bcrypt")
.get(get_page)
.post(post_page),
)
// HMAC-based CSRF protection
.push(
Router::with_hoop(hmac_csrf)
.path("hmac")
.get(get_page)
.post(post_page),
)
// AES-GCM-based CSRF protection
.push(
Router::with_hoop(aes_gcm_session_csrf)
.path("aes_gcm")
.get(get_page)
.post(post_page),
)
// ChaCha20Poly1305-based CSRF protection
.push(
Router::with_hoop(ccp_session_csrf)
.path("ccp")
.get(get_page)
.post(post_page),
);
// Start server on port 8698
let acceptor = TcpListener::new("0.0.0.0:8698").bind().await;
Server::new(acceptor).serve(router).await;
}
// Helper function to generate HTML page with CSRF token and message
fn get_page_html(csrf_token: &str, msg: &str) -> String {
format!(
r#"
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>Csrf SessionStore</title></head>
<body>
<h2>Csrf Example: SessionStore</h2>
<ul>
<li><a href="/bcrypt/">Bcrypt</a></li>
<li><a href="/hmac/">Hmac</a></li>
<li><a href="/aes_gcm/">Aes Gcm</a></li>
<li><a href="/ccp/">chacha20poly1305</a></li>
</ul>
<form action="./" method="post">
<input type="hidden" name="csrf_token" value="{csrf_token}" />
<div>
<label>Message:<input type="text" name="message" /></label>
</div>
<button type="submit">Send</button>
</form>
<pre>{msg}</pre>
</body>
</html>
"#
)
}
[package]
name = "example-csrf-session-store"
version.workspace = true
edition.workspace = true
publish.workspace = true
rust-version.workspace = true
[dependencies]
salvo = { workspace = true, features = ["csrf", "session"] }
tokio = { workspace = true, features = ["macros"] }
tracing.workspace = true
tracing-subscriber.workspace = true
serde = { workspace = true, features = ["derive"] }