前言

我写这篇文章,是在2026年,这个时候,编程智能体已经能够完成人类布置的80%以上的编程任务。特别是在很多自媒体的加持下,“人人会编程”的理念充斥着社会空间的每一个角落。的确,我们能够让Codex,Claude Code,Qoder,CodeWhale这样的编程智能体,将我们的需求(自然语言)翻译成程序代码,这些程序代码不仅能跑起来,其展示的效果还相当”惊人“。
可作为程序员,要保持冷静,我们的责任远不是辅助编程智能体来生成代码。当大家都在为编程智能体生成代码的 “看得见效果” 而欢呼的同时,我们要比之前任何时候都要了解那些深藏于水底的 ”看不见“ 东西。
当人人都在用Codex创建web应用时,有多少人了解 CSRF漏洞
今天,我要回顾的就是 CSRF漏洞

CSRF 简介

CSRF(Cross-Site Request Forgery,跨站请求伪造),也被称为 XSRFOne-Click Attack。它是一种经典且基础的安全漏洞。
在这里插入图片描述

CSRF 的本质是利用浏览器自动携带 Cookie 的机制,冒用用户已登录的身份向目标网站发送伪造请求。 攻击者不需要知道用户的密码,也不需要入侵目标服务器,只需要诱导用户在已登录的状态下访问恶意页面即可。 目标网站仅凭 Cookie 验证身份,无法判断请求是否为用户真实意图,从而被欺骗执行了非授权操作。

一个经典的,教科书级别的CSRF攻击场景

你登录了银行网站 A,然后在一个论坛里点击了一个看起来无害的链接。这个链接实际上是一个构造了转账请求的<img> 标签。浏览器自动附带了你的 Cookie,银行后台看到的是合法用户的请求,于是转账执行成功。

CSRF 攻击的三个必要条件

  1. 用户已登录目标网站(浏览器中保有有效的身份凭证,如 Session Cookie)。
  2. 攻击者能构造出目标网站的有效请求(请求参数可预测或可枚举)。
  3. 目标网站没有对请求来源做有效校验(如缺少 CSRF Token、SameSite 等防护机制)。

下面的四种防御思路,本质上就是分别破坏上述三个条件中的一条或多条。当其中一条被打破,CSRF攻击就无法成立。

核心防御思路

  1. Anti-CSRF Token:每个敏感请求附带一个随机、与用户会话绑定的 Token,服务端校验;破坏条件2(请求不可构造/请求变了)。
  2. SameSite Cookie:设置 Cookie 的 SameSite 属性为 LaxStrict,阻止第三方请求携带 Cookie;破坏条件1(Cookie不自动携带)。
  3. 双重验证:操作前要求输入密码、验证码或二次确认;破坏条件3(新增校验)。
  4. 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 之前,中间件会:

  1. 解析请求中的 resume_agent_user_id Cookie
  2. 验证 HMAC 签名
  3. 查 Redis 确认用户存在
  4. 验证失败 → 直接返回 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多年前,访问量也是相当火爆的。

参考

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐