在AI编程时代,了解CSRF
前言
我写这篇文章,是在2026年,这个时候,编程智能体已经能够完成人类布置的80%以上的编程任务。特别是在很多自媒体的加持下,“人人会编程”的理念充斥着社会空间的每一个角落。的确,我们能够让Codex,Claude Code,Qoder,CodeWhale这样的编程智能体,将我们的需求(自然语言)翻译成程序代码,这些程序代码不仅能跑起来,其展示的效果还相当”惊人“。
可作为程序员,要保持冷静,我们的责任远不是辅助编程智能体来生成代码。当大家都在为编程智能体生成代码的 “看得见效果” 而欢呼的同时,我们要比之前任何时候都要了解那些深藏于水底的 ”看不见“ 东西。
当人人都在用Codex创建web应用时,有多少人了解 CSRF漏洞?
今天,我要回顾的就是 CSRF漏洞 。
CSRF 简介
CSRF(Cross-Site Request Forgery,跨站请求伪造),也被称为 XSRF 或 One-Click Attack。它是一种经典且基础的安全漏洞。
CSRF 的本质是利用浏览器自动携带 Cookie 的机制,冒用用户已登录的身份向目标网站发送伪造请求。 攻击者不需要知道用户的密码,也不需要入侵目标服务器,只需要诱导用户在已登录的状态下访问恶意页面即可。 目标网站仅凭 Cookie 验证身份,无法判断请求是否为用户真实意图,从而被欺骗执行了非授权操作。
一个经典的,教科书级别的CSRF攻击场景
你登录了银行网站 A,然后在一个论坛里点击了一个看起来无害的链接。这个链接实际上是一个构造了转账请求的
<img>标签。浏览器自动附带了你的 Cookie,银行后台看到的是合法用户的请求,于是转账执行成功。
CSRF 攻击的三个必要条件
- 用户已登录目标网站(浏览器中保有有效的身份凭证,如 Session Cookie)。
- 攻击者能构造出目标网站的有效请求(请求参数可预测或可枚举)。
- 目标网站没有对请求来源做有效校验(如缺少 CSRF Token、SameSite 等防护机制)。
下面的四种防御思路,本质上就是分别破坏上述三个条件中的一条或多条。当其中一条被打破,CSRF攻击就无法成立。
核心防御思路
- Anti-CSRF Token:每个敏感请求附带一个随机、与用户会话绑定的 Token,服务端校验;破坏条件2(请求不可构造/请求变了)。
- SameSite Cookie:设置 Cookie 的
SameSite属性为Lax或Strict,阻止第三方请求携带 Cookie;破坏条件1(Cookie不自动携带)。 - 双重验证:操作前要求输入密码、验证码或二次确认;破坏条件3(新增校验)。
- Referer / Origin 校验:检查请求来源是否为可信站点;破坏条件3(校验来源)。
实战
以下实战代码来至于我的简历智能体(https://www.craftaidhub.com/resume_app/)项目。
一、核心 CSRF 防御:SameSite Cookie
文件:src/security/cookie.rs 第 64 行
Cookie::build(self.cookie_name.clone(), user_id.to_string())
.http_only(true)
.secure(false)
.same_site(SameSite::Strict) // ← 关键:严格同站模式
.max_age(cookie_duration)
.finish()
SameSite::Strict 是当前最有效的 CSRF 防御手段之一:
- 浏览器在来自其他站点的跨站请求(包括通过
<form>提交的 POST、通过<img>发起的 GET)中,不会附带resume_agent_user_id这个 Cookie。 - 攻击者构造伪造页面 → 用户浏览器自动发起请求 → 因 Cookie 未携带 → 中间件无法解析 user_id → 请求被拦截。
效果: 当用户登录某钓鱼网站,该网站通过<form action="https://your-server/api/v1/startAnalyze" method="POST"> 发起跨站请求时,用户的身份 Cookie 不会被带上,因此无法冒充用户提交分析请求。
二、第二层:Cookie 签名(防篡改)
文件:src/security/cookie.rs 第 136–184 行
fn sign_cookie(&self, cookie: &mut Cookie<'_>) -> Result<(), AppError> {
let mut mac = HmacSha256::new_from_slice(&self.secret_key)?;
mac.update(message.as_bytes());
let signature = hex::encode(mac.finalize().into_bytes());
let signed_value = format!("{}.{}", cookie.value(), signature);
cookie.set_value(signed_value);
}
- 使用 HMAC-SHA256 对 cookie_name=cookie_value 计算签名
- 签名附加在 Cookie 值末尾,格式为 user_id.signature
- 验证时使用 恒定时间比较(subtle::ConstantTimeEq)防止时序攻击
CSRF 关联: 即使攻击者通过某种方式强行设置了 Cookie(如 XSS 或其他漏洞写入了 resume_agent_user_id),因为无法伪造 HMAC 签名,中间件在验证时会拒绝该请求。
三、第三层:请求身份验证中间件
文件:src/middleware.rs 第 69–166 行
// 在 SecurityMiddleware 中
if path.starts_with("/api/v1/")
&& !path.starts_with("/api/v1/status")
&& !path.starts_with("/api/v1/feedback/question")
&& !path.starts_with("/api/v1/feedback/submit")
{
match app_state.cookie_security
.extract_user_id_from_request(&http_req, &*app_state.user_repo)
.await
{
Ok(Some(user_id)) => { /* 继续 */ },
Ok(None) => { /* 新用户或无效用户,不设置 user_id */ },
Err(e) => { return Err(BadRequest("Invalid user session")); }
}
}
所有受保护的 API 接口(/api/v1/startAnalyze、/api/v1/analyze、/api/v1/diagnosis、/api/v1/resume/* 等)在进入 handler 之前,中间件会:
- 解析请求中的 resume_agent_user_id Cookie
- 验证 HMAC 签名
- 查 Redis 确认用户存在
- 验证失败 → 直接返回 400 / 401
这意味着跨站请求即使走到了中间件层,也会因为签名验证失败或用户不存在而被拒绝。
四、辅助防御:速率限制
文件:src/security/rate_limit.rs
/// 检查用户是否超过速率限制
pub fn check_user_rate_limit(&self, user_id: &str) -> Result<bool, AppError> {
...
// 如果超过窗口时间,重置计数
if now.duration_since(request_info.last_request) > self.window_size {
request_info.count = 0;
request_info.last_request = now;
}
// 检查是否超过限制
if request_info.count >= self.system_limits.max_requests_per_user {
warn!("Rate limit exceeded for user: {}", user_id);
return Ok(false);
}
...
}
五、辅助防御:输入内容安全验证
文件:src/security/rate_limit.rs 第 200+ 行
fn contains_suspicious_content(input: &str) -> bool {
// 扫描 <script>、<iframe>、onerror=、javascript: 等 XSS 特征
}
在 startAnalyze、diagnosis 等 POST 接口的 handler 中,对 JD 和简历内容进行安全过滤。这防止了攻击者通过 CSRF 漏洞注入恶意脚本执行 XSS。
六、辅助防御:反馈提交动态密钥
文件:src/middleware/feedback_validation.rs
impl<S, B> actix_web::dev::Service<ServiceRequest> for FeedbackValidationMiddlewareService<S>
where
S: actix_web::dev::Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>
+ 'static,
S::Future: 'static,
B: MessageBody + 'static,
{
fn call(&self, req: ServiceRequest) -> Self::Future {
let app_state = self.app_state.clone();
let service = self.service.clone();
Box::pin(async move {
// Only validate for feedback submission endpoint
if req.path() == "/api/v1/feedback/submit" && req.method() == "POST" {
// Parse the request body to get dynamic_key
match req.json::<serde_json::Value>().await {
Ok(json_body) => {
if let Some(dynamic_key) = json_body.get("dynamic_key").and_then(|k| k.as_str()) {
// Validate the dynamic key
match app_state.feedback_service.validate_dynamic_key(dynamic_key).await {
Ok(true) => {
...
}
Ok(false) => {
// Key is invalid or expired
...
}
Err(e) => {
error!("Failed to validate dynamic key: {}", e);
...
}
}
} else {
...
}
}
Err(e) => {
...
}
}
}
...
})
}
}
/api/v1/feedback/submit 接口在 FeedbackValidationMiddleware 中验证 dynamic_key:
- 客户端提交反馈时必须先请求 GET /api/v1/feedback/question 获取一个带有 TTL 的动态密钥
- 提交时中间件验证该密钥
- 密钥过期或在别处使用过都会失败
这为反馈提交接口提供了一个 CSRF token 的等效实现。
后记
我写这篇文章的目的主要是为了记录,所以结束得比较冲忙,在AI编程时代,感觉程序员的时间不是变多了,反而是变少了。
同时,参考的链接也记录了下来,也是很有价值的。这些链接,要是放在10多年前,访问量也是相当火爆的。
参考
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)