CSRF漏洞保护
注意:跨站请求伪造CSRF, 域名要保持一致才能成功
demo地址
简介
CSRF(Cross-Site Request Forgery 跨站请求伪造),也可称为一键式攻击 (one-click-attack),通常缩写为 CSRF
或者 XSRF
。
CSRF
攻击是一种挟持用户在当前已登录的浏览器上发送恶意请求的攻击方法。 相对于XSS利用用户对指定网站的信任,CSRF则是利用网站对用户网页浏览器的信任。
简单来说CSRF是致击者通过一些技术手段欺骗用户的浏览器,去访问一个用户曾经认证过的网站并执行恶意请求,例如发送邮件、发消息、甚至财产操作(如转账和购买商品)。由于客户端浏览器)已经在该网站上认证过,所以该网站会认为是真正用户在操作而执行请求(实际上这个并非用户的本意)。
举个简单的例子😍 假设 wkk 现在登录了某银行的网站准备完成一项转账操作,转账的链接如下:
https: //bank .xxx .com/withdraw?account=wkk&amount=2000&for=zhangsan
可以看到,这个链接是想从 wkk 这个账户下转账 2000 元到 zhangsan 账户下,假设wkk没有注销登录该银行的网站,就在同一个浏览器新的选项卡中打开了一个危险网站,这个危险网站中有一幅图片,代码如下:
https: //bank .xxx .com/withdraw?account=wkk&amount=2000&for=lisi
一旦用户打开了这个网站,这个图片链接中的请求就会自动发送出去。由于是同一个浏览器并且用户尚未注销登录,所以该请求会自动携带上对应的有效的 Cookie 信息,进而完成一次转账操作。这就是跨站请求伪造。
CSRF攻击演示
创建银行应用
- 引入依赖
java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- 修改配置
java
@Configuration //自定义 security 的配置
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//内存数据源
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
return inMemoryUserDetailsManager;
}
//指定全局 AuthenticationManager 使用内存数据源
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and().formLogin()
.and()
// .csrf().disable(); //关闭 CSRF 跨站请求保护
.csrf(); //开始 CSRF 防御
}
}
- 创建 controller 并启动启动
java
@RestController
public class HelloController {
@PostMapping("/withdraw")
public String withdraw() {
System.out.println("执行一次转账操作");
return "执行一次转账操作";
}
}
创建恶意应用
- 创建简单 springboot 应用
- 修改配置
java
server.port=8081
- 准备攻击页面
html
<form action="http://localhost:8080/withdraw" method="post">
<input name="name" type="hidden" value="wkk"/>
<input name="money" type="hidden" value="10000">
<input type="submit" value="点我">
</form>
CSRF 防御
CSRF 攻击的根源在于浏览器默认的身份验证机制 (自动携带当前网站的Cookie信息),这种机制虽然可以保证请求是来自用户的某个浏览器,但是无法确保这请求是用户授权发送。
攻击者和用户发送的请求一模一样,这意味着我们没有办法去直接拒绝这里的某一个请求。如果能在合法请求中额外携带一个攻击者无法获取的参数,就可以成功区分出两种不同的请求,进而直接拒绝掉恶意请求。
在 SpringSecurity 中就提供了这种机制来防御 CSRF 攻击,这种机制我们称之为令牌同步模式
。
令牌同步模式
这是目前主流的 CSRF 攻击防御方案。具体的操作方式就是在每一个 HTTP 请求中,除了默认自动携带的 Cookie 参数之外,再提供一个安全的、随机生成的字符串,我们称之为 CSRF 令牌。
这个 CSRF 令牌由服务端生成,生成后在 HttpSession 中保存一份。
当前端请求到达后,将请求携带的 CSRF 令牌信息和服务端中保存的令牌进行对比,如果两者不相等,则拒绝掉该 HTTP 请求。
注意: 考虑到会有一些外部站点链接到我们的网站,所以我们要求请求是幂等的,这样对于HEAD、OPTIONS、TRACE 等方法就没有必要使用 CSRF 令牌了,强行使用可能会导致令牌泄露!
java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.
...
formLogin()
.and()
.csrf(); //开启 csrf
}
}
查看登录页面源码
开启csrf后会自动生成csrf令牌

传统web开发使用CSRF (只需要在配置中设置开启csrf即可)
开启CSRF防御后会自动在提交的表单中加入如下代码,如果不能自动加入,需要在开启之后手动加入如下代码,并随着请求提交。获取服务端令牌方式如下:
js
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>

添加依赖
java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
配置 thymeleaf
java
spring.thymeleaf.cache=false
spring.thymeleaf.mode=HTML
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
开发测试 controller
java
@Controller
//主页index跳转这里不能用@RestController
public class HelloController {
@PostMapping("/hello")
@ResponseBody
public String hello() {
System.out.println("hello ok!");
return "Hello ok";
}
@GetMapping("/index")
public String index() {
return "index";
}
}
创建 html
js
<html lang="en">
<head>
<meta charset="UTF-8">
<title>测试 CSRF 防御</title>
</head>
<body>
<form th:action="@{/hello}" method="post">
<input type="submit" value="hello"/>
</form>
</body>
</html>
测试查看index.html源码

前后端分离使用 CSRF
前后端分离开发时,只需要将生成 csrf 放入到cookie 中,并在请求时获取 cookie 中令牌信息进行提交即可
修改 CSRF 存入 Cookie
默认开启的csrf保存在session作用域中,不适合前后端分离系统,所以开启csrf后要指定csrf令牌存储方式
java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf()
//把csrf保存到前端的cookie中
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
注意:csrf保存到cookie,是否会存在cookie恶意请求攻击?
不会。csrf原理中首先会看携带的cookie,进而要求请求中必须携带什么样的格式携带cookie才可以。
csrf会要求请求头header中有key和value,即使拿到cookie但是不知道怎么组装header中的key和value也不行。

开启了csrf就要先验证请求的令牌是否合法,首先看header里面有没有传递csrf,同时还要进行比对。

发送请求携带令牌
- 请求参数中携带令牌
java
key: _csrf
value:"xxx"
- 请求头中携带令牌
java
X-XSRF-TOKEN:value

引用 编程不良人