代码审计之CSRF

admin 2025年4月8日00:05:26评论0 views字数 6770阅读22分34秒阅读模式

介绍

本篇为代码审计系列的第十篇《代码审计之CSRF》,预计本系列为30篇左右。

CSRF跨站请求伪造,用一句话来概括的就是:攻击者诱使已认证的用户在不知情的情况下执行非预期的操作。

大体步骤为:

1、用户登录了A网站,浏览器存储了A网站的Cookie相关认证信息。

2、攻击者伪造了一个A网站的敏感请求,在A网站Cookie信息还未失效期间,诱导用户访问我们构造的敏感请求。从而达成攻击。

未做任何防护的情况

比如下面这段代码,用来模拟转账操作

@Controller
publicclassBankController{

@PostMapping("/transfer")
public String transferMoney(@RequestParam String toAccount, @RequestParam double amount){
// 执行转账逻辑
        System.out.println("Transferring $" + amount + " to " + toAccount);
return"redirect:/success";
    }
}

方法接收到被转账的账号和金额后,进行转账,此时这段代码未作CSRF校验。比如用户登录了目标网站,Cookie还未失效,此时攻击者可构造一个钓鱼页面,例如下面这个代码:

<!-- 攻击者的恶意网站 -->
<html>
<body>
<h1>免费领取奖品!</h1>
<formaction="http://victim-bank.com/transfer"method="POST">
<inputtype="hidden"name="toAccount"value="attacker-account">
<inputtype="hidden"name="amount"value="1000">
</form>
<script>
document.forms[0].submit();
</script>
</body>
</html>

当受害者在同一浏览器访问时,浏览器会携带目标网站的Cookie信息,这个请求就会被成功执行。

Referer修复

针对CSRF的情况,有一种修复方式是Referer头,它的原理在于执行请求前先验证这个请求的Referer头地址,看是否是自己允许的地址,比如下面这个示例代码,在转账前先验证了Referer头是否为空,或者是否是指定的URL开头,即只允许Referer是来自于自己站内的连接。

@Controller
publicclassSecureBankController{

@PostMapping("/secure-transfer")
public String secureTransferMoney(@RequestParam String toAccount, @RequestParam double amount, HttpServletRequest request){

// 检查Referer头
        String referer = request.getHeader("Referer");
if (referer == null || !referer.startsWith("https://victim-bank.com")) {
thrownew SecurityException("Invalid request origin");
        }

// 执行转账逻辑
        System.out.println("Transferring $" + amount + " to " + toAccount);
return"redirect:/success";
    }
}

这种验证方式看起来是没什么问题的,但也可能存在一些绕过方式,比如逻辑是检测是否是example.com开头,攻击者可以去注册一个域名,把子域名设置成example.com,去绕过校验逻辑,或者如果目标站点存在重定向漏洞,则可以利用重定向去执行敏感请求等。

所以严格上来看,这种方式是不推荐的。

CSRF Token

这种机制在于服务器会为每个会话生成一个唯一的Token,token会通过表单隐藏字段或者Cookie发送给客户端,客户端提交相关请求时,必须携带这个token,然后服务端会校验token是否一致,此时攻击者是无法猜测出目标用户的Token的。

注意这个分为会话级别和请求级别,通常一般的系统会话级别就可以,即用户登录后就生成一个随机Token存到Session中,后续相关请求页面加载时就把这个Token带上,后端去判断是否一致,这个所有请求都用这一个Token。

也有那种系统安全性高的,每次请求都会去刷新Token,我们下面分别来看下它们的示例。

先来看系统级别,比如用户登录成功就设置Token,退出则失效,示例代码如下。

@Controller
publicclassAuthController{

// 用户登录时生成并存储 Token
@PostMapping("/login")
public String login(HttpServletRequest request, @RequestParam String username, @RequestParam String password){

// 1. 模拟用户登录操作
if (!"admin".equals(username) || !"123456".equals(password)) {
return"redirect:/login?error";
        }

// 2. 登录后,生成会话级 CSRF Token
        String csrfToken = UUID.randomUUID().toString();
        request.getSession().setAttribute("csrfToken", csrfToken);

// 3. 除了放到Session中,也可以放到Cookie中,但是Cookie要注意,如果存在XSS,Cookie能泄露,则Token也就泄露了
        response.addCookie(new Cookie("CSRF-TOKEN", csrfToken));

return"redirect:/dashboard";
    }

// 退出的时候清除 Token
@GetMapping("/logout")
public String logout(HttpServletRequest request){
        request.getSession().removeAttribute("csrfToken");
return"redirect:/login";
    }
}

然后比如用户发送请求,如下示例,加载页面时,把Token当作一个隐藏字段一起返回到前端页面:

@Controller
publicclassFormController{

@GetMapping("/form")
public String showForm(HttpServletRequest request, Model model){
// 从会话中获取现有 Token
        String csrfToken = (String) request.getSession().getAttribute("csrfToken");
        model.addAttribute("csrfToken", csrfToken);
return"form-page";
    }
}

前端页面大体长这个样子:

<!-- form-page.html -->
<formaction="/submit"method="post">
<!-- 使用会话中的 Token -->
<inputtype="hidden"name="_csrf"th:value="${csrfToken}">

<inputtype="text"name="data">
<buttontype="submit">提交</button>
</form>

此时页面请求就会带着这个Token,这个是攻击者无法伪造的,后端逻辑只需要检测这个是否和当前用户匹配即可。

@PostMapping("/submit")
public Map<String, Object> doTransferToken(HttpServletRequest request, HttpSession session){
    String token = request.getParameter("_csrf");
    String sessionToken = (String) session.getAttribute("csrfToken");

// 校验 Token 有效性
if (serverToken == null || !serverToken.equals(clientToken)) {
return ResponseEntity.status(403).body("CSRF Token 验证失败");
    }

// 验证通过后处理业务逻辑
return ResponseEntity.ok("操作成功");
}

Spring Security CSRF Token

上面是通过自己手动实现的CSRF校验机制,如何使用了一些安全框架,通常会带有这方面的功能,比如SpringSecurity。

如果项目使用了SpringSecurity,那么csrf是默认就启用的,甚至不用在配置文件中显式的去配置,比如我们有个小demo示例,它有个form表单,输入内容后,后端控制器会将内容打印到网页上。

然后我们使用SpringSecurity,配置内容如下:

package org.example.springsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
publicclassSecurityConfig{

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)throws Exception {
        http
                .authorizeHttpRequests(auth -> auth
                        .anyRequest().authenticated()
                )
                .formLogin(form -> form
                        .permitAll()
                );
return http.build();
    }
}

这个配置文件很简单,就配置了form可以不需要验证访问,其它路径都需要验证,并没有显式配置csrf,然后控制器代码如下。

@Controller
publicclassFormController{

@GetMapping("/")
public String showForm(){
return"index";
    }

@PostMapping("/submit")
public String processForm(
            @RequestParam String content,
            Model model
    )
{
        model.addAttribute("inputContent", content);
return"result";
    }
}

访问/根目录时就显式index页面,index就是一个form表单,内容如下:

<!DOCTYPE html>
<htmlxmlns:th="http://www.thymeleaf.org">
<head>
<metacharset="UTF-8">
<title>CSRF测试表单</title>
</head>
<body>
<h1>输入测试内容</h1>
<formmethod="post"th:action="@{/submit}">
<inputtype="text"name="content"required>
<inputtype="submit"value="提交">
</form>
</body>
</html>

可以看到并没有csrf配置,但访问页面查看源代码,发现有csrf,如下图。

代码审计之CSRF

它名字默认叫_csrf,每次刷新都会变。这里可以测试下,比如手动在网页源码更改下csrf的值,提交请求后会提示403错误。

代码审计之CSRF

因为csrf主要影响在于更改一些请求和状态,所以向get类请求就不会增加csrf验证,这个参考如下:

默认受保护的请求:POST、PUT、PATCH、DELETE(会改变状态的请求)。

不受保护的请求:GET、HEAD、OPTIONS、TRACE(不会改变状态的请求)。

如果是接口形式,则需要手动配置,首先在SpringSecurity中配置下csrf,示例如下:

@Configuration
@EnableWebSecurity
publicclassSecurityConfig{

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)throws Exception {
        http
                .authorizeHttpRequests(auth -> auth
                        .anyRequest().authenticated()
                )
                .csrf(csrf -> csrf
                        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                )
                .formLogin(form -> form
                        .permitAll()
                );
return http.build();
    }
}

主要增加了.csrf部分,这个.csrf就代表SpringSecurity中csrf的配置项,这里主要配置了withHttpOnlyFalse,它代表会把csrf的值加到cookie中,且关闭了cookie的httponly,因为后续接口发送请求时,需要从cookie中拿这个值,开启httponly则会禁止js读取。

之后接口请求时需要获取csrf值,这个值的名字默认叫做XSRF-TOKEN,然后发送请求时,需要将其加到headers头中,加的时候名字要叫X-XSRF-TOKEN,可参考如下示例。

// 从Cookie中提取CSRF令牌
functiongetCsrfTokenFromCookie({
const value = `; ${document.cookie}`;
const parts = value.split(`; XSRF-TOKEN=`);
if (parts.length === 2return parts.pop().split(';').shift();
returnnull;
}

// 发送POST请求时在请求头中包含CSRF令牌
fetch('/api/test', {
method'POST',
headers: {
'Content-Type''application/json',
'X-XSRF-TOKEN': csrfToken // 添加CSRF令牌到请求头
   },
bodyJSON.stringify(data),
credentials'include'// 确保包含Cookie
});

所以在审计时要看系统是否用了SpringSecurity,是否有接口开发,前端JS请求时是否有带CSRF信息。

以上就是关于代码审计中CSRF漏洞的相关讲解。

关于我们

我们是《AI安全攻防》,致力于分享AI安全、渗透测试、代码审计等内容,欢迎您的关注!

原文始发于微信公众号(AI安全攻防):代码审计之CSRF

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年4月8日00:05:26
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   代码审计之CSRFhttps://cn-sec.com/archives/3912097.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息