介绍
本篇为代码审计系列的第十篇《代码审计之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的值,提交请求后会提示403错误。
因为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 === 2) return parts.pop().split(';').shift();
returnnull;
}
// 发送POST请求时在请求头中包含CSRF令牌
fetch('/api/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': csrfToken // 添加CSRF令牌到请求头
},
body: JSON.stringify(data),
credentials: 'include'// 确保包含Cookie
});
所以在审计时要看系统是否用了SpringSecurity,是否有接口开发,前端JS请求时是否有带CSRF信息。
以上就是关于代码审计中CSRF漏洞的相关讲解。
关于我们
我们是《AI安全攻防》,致力于分享AI安全、渗透测试、代码审计等内容,欢迎您的关注!
原文始发于微信公众号(AI安全攻防):代码审计之CSRF
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论