• Spring Security CSRF 保护指南


    1. 概述

    在本教程中,我们将讨论跨站点请求伪造(CSRF)攻击以及如何使用Spring Security防止它们。

    延伸阅读:

    使用Spring MVC和Thymeleaf进行CSRF保护

    使用Spring Security,Spring MVC和Thymeleaf防止CSRF攻击的快速实用指南。

    Spring 引导安全自动配置

    Spring Boot 默认 Spring Security 配置的快速实用指南。

    弹簧方法安全性简介

    使用 Spring 安全性框架的方法级安全性指南。

    2. 两种简单的CSRF攻击

    CSRF 攻击有多种形式。让我们讨论一些最常见的。

    2.1. GET示例

    让我们考虑登录用户用于将资金转移到特定银行账户1234 的以下GET请求:

    GET http://bank.com/transfer?accountNo=1234&amount=100

    如果攻击者想将钱从受害者的帐户转移到他自己的帐户(5678),他需要让受害者触发请求:

    GET http://bank.com/transfer?accountNo=5678&amount=1000

    有多种方法可以实现这一点:

    • 链接–攻击者可以说服受害者单击此链接,例如,执行传输:
    • 图像 – 攻击者可能使用带有目标 URL 的 tag 作为图像源。换句话说,甚至不需要点击。当页面加载时,请求将自动执行:

    2.2. POST示例

    假设主请求需要是一个 POST 请求:

    1. POST http://bank.com/transfer
    2. accountNo=1234&amount=100

    在这种情况下,攻击者需要让受害者运行类似的请求:

    1. POST http://bank.com/transfer
    2. accountNo=5678&amount=1000

    在这种情况下,标签都不起作用。

    攻击者需要<form>

    但是,可以使用JavaScript自动提交表单:

    1. ...

    2.3. 实际模拟

    现在我们了解了CSRF攻击的样子,让我们在Spring应用程序中模拟这些示例。

    我们将从一个简单的控制器实现开始 —BankController

    1. @Controller
    2. public class BankController {
    3. private Logger logger = LoggerFactory.getLogger(getClass());
    4. @RequestMapping(value = "/transfer", method = RequestMethod.GET)
    5. @ResponseBody
    6. public String transfer(@RequestParam("accountNo") int accountNo,
    7. @RequestParam("amount") final int amount) {
    8. logger.info("Transfer to {}", accountNo);
    9. ...
    10. }
    11. @RequestMapping(value = "/transfer", method = RequestMethod.POST)
    12. @ResponseStatus(HttpStatus.OK)
    13. public void transfer2(@RequestParam("accountNo") int accountNo,
    14. @RequestParam("amount") final int amount) {
    15. logger.info("Transfer to {}", accountNo);
    16. ...
    17. }
    18. }

    让我们还有一个触发银行转帐操作的基本 HTML 页面:

    1. CSRF test on Origin

    这是在源域上运行的主应用程序的页面。

    我们应该注意,我们已经通过一个简单的链接实现了GET,并通过一个简单的<form>实现了POST

    现在让我们看看攻击者页面会是什么样子:

    此页面将在另一个域(攻击者域)上运行。

    最后,让我们在本地运行原始应用程序和攻击者应用程序。

    要使攻击起作用,需要使用会话 cookie 对原始应用程序对用户进行身份验证。

    让我们首先访问原始应用程序页面:

    http://localhost:8081/spring-rest-full/csrfHome.html

    它将在我们的浏览器上设置JSESSIONIDcookie。

    然后让我们访问攻击者页面:

    http://localhost:8081/spring-security-rest/api/csrfAttacker.html

    如果我们跟踪来自此攻击者页面的请求,我们将能够发现命中原始应用程序的请求。由于JSESSIONIDcookie 会自动与这些请求一起提交,Spring 会像它们来自原始域一样对其进行身份验证。

    3. 弹簧MVC应用

    为了保护MVC应用程序,Spring为每个生成的视图添加一个CSRF令牌。此令牌必须在修改状态(PATCH、POST、PUT 和 DELETE - 而不是 GET)的每个 HTTP 请求上提交到服务器。这可以保护我们的应用程序免受 CSRF 攻击,因为攻击者无法从自己的页面获取此令牌。

    接下来,我们将了解如何配置应用程序安全性以及如何使客户端符合它。

    3.1. 弹簧安全配置

    在较旧的XML配置(Spring Security 4之前),CSRF保护默认被禁用,我们可以根据需要启用它:

    1. ...

    从 Spring Security 4.x 开始,CSRF 保护默认处于启用状态。

    此默认配置将 CSRF 令牌添加到名为 _csrf 的HttpServletRequest属性中。

    如果需要,我们可以禁用此配置:

    1. @Bean
    2. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    3. http
    4. .csrf().disable();
    5. return http.build();
    6. }

    3.2. 客户端配置

    现在我们需要在请求中包含 CSRF 令牌。

    _csrf属性包含以下信息:

    • 令牌 – CSRF 令牌值
    • 参数名称 – HTML 表单参数的名称,必须包含令牌值
    • 标头名称 – HTTP 标头的名称,必须包含令牌值

    如果我们的视图使用 HTML 表单,我们将使用参数 Name令牌值来添加隐藏的输入:

    如果我们的视图使用 JSON,我们需要使用headerName令牌值来添加 HTTP 标头。

    我们首先需要在元标记中包含令牌值和标头名称:

    然后让我们使用 JQuery 检索元标记值:

    1. var token = $("meta[name='_csrf']").attr("content");
    2. var header = $("meta[name='_csrf_header']").attr("content");

    最后,让我们使用这些值来设置 XHR 标头:

    1. $(document).ajaxSend(function(e, xhr, options) {
    2. xhr.setRequestHeader(header, token);
    3. });

    4. 无状态弹簧 API

    让我们回顾一下前端使用的无状态 Spring API 的情况。

    正如我们在专门文章中所解释的,我们需要了解我们的无状态 API 是否需要 CSRF 保护。

    如果我们的无状态 API 使用基于令牌的身份验证,例如 JWT,我们不需要 CSRF 保护,我们必须禁用它,如前所述。

    但是,如果我们的无状态 API 使用会话 cookie 身份验证,我们需要启用 CSRF 保护,如下文所示。

    4.1. 后端配置

    我们的无状态 API 不能像我们的 MVC 配置那样添加 CSRF 令牌,因为它不会生成任何 HTML 视图。

    在这种情况下,我们可以使用CookieCsrfTokenRepository在cookie中发送CSRF令牌:

    1. @Configuration
    2. public class SpringSecurityConfiguration {
    3. @Bean
    4. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    5. http
    6. .csrf()
    7. .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    8. return http.build();
    9. }
    10. }

    此配置将在前端设置XSRF-TOKENcookie。由于我们将仅HTTP标志设置为false,因此前端将能够使用JavaScript检索此cookie。

    4.2. 前端配置

    使用 JavaScript,我们需要从document.cookie列表中搜索XSRF-TOKENcookie 值。

    由于此列表存储为字符串,因此我们可以使用以下正则表达式检索它:

    const csrfToken = document.cookie.replace(/(?:(?:^|.*;\s*)XSRF-TOKEN\s*\=\s*([^;]*).*$)|^.*$/, '$1');

    然后,我们必须将令牌发送到每个修改 API 状态的 REST 请求:POST、PUT、DELETE 和 PATCH。

    Spring 希望在X-XSRF-TOKEN标头中接收它。

    我们可以简单地使用 JavaScriptFetchAPI 进行设置:

    1. fetch(url, {
    2. method: 'POST',
    3. body: /* data to send */,
    4. headers: { 'X-XSRF-TOKEN': csrfToken },
    5. })

    5. CSRF 禁用测试

    完成所有这些操作后,让我们进行一些测试。

    让我们首先尝试在 CSRF 被禁用时提交一个简单的 POST 请求:

    1. @ContextConfiguration(classes = { SecurityWithoutCsrfConfig.class, ...})
    2. public class CsrfDisabledIntegrationTest extends CsrfAbstractIntegrationTest {
    3. @Test
    4. public void givenNotAuth_whenAddFoo_thenUnauthorized() throws Exception {
    5. mvc.perform(
    6. post("/foos").contentType(MediaType.APPLICATION_JSON)
    7. .content(createFoo())
    8. ).andExpect(status().isUnauthorized());
    9. }
    10. @Test
    11. public void givenAuth_whenAddFoo_thenCreated() throws Exception {
    12. mvc.perform(
    13. post("/foos").contentType(MediaType.APPLICATION_JSON)
    14. .content(createFoo())
    15. .with(testUser())
    16. ).andExpect(status().isCreated());
    17. }
    18. }

    在这里,我们使用一个基类来保存通用的测试帮助程序逻辑 —CsrfAbstractIntegrationTest

    1. @RunWith(SpringJUnit4ClassRunner.class)
    2. @WebAppConfiguration
    3. public class CsrfAbstractIntegrationTest {
    4. @Autowired
    5. private WebApplicationContext context;
    6. @Autowired
    7. private Filter springSecurityFilterChain;
    8. protected MockMvc mvc;
    9. @Before
    10. public void setup() {
    11. mvc = MockMvcBuilders.webAppContextSetup(context)
    12. .addFilters(springSecurityFilterChain)
    13. .build();
    14. }
    15. protected RequestPostProcessor testUser() {
    16. return user("user").password("userPass").roles("USER");
    17. }
    18. protected String createFoo() throws JsonProcessingException {
    19. return new ObjectMapper().writeValueAsString(new Foo(randomAlphabetic(6)));
    20. }
    21. }

    我们应该注意,当用户拥有正确的安全凭证时,请求已成功执行 - 不需要额外的信息。

    这意味着攻击者可以简单地使用前面讨论的任何攻击媒介来破坏系统。

    6. 支持CSRF的测试

    现在让我们启用 CSRF 保护并查看区别:

    1. @ContextConfiguration(classes = { SecurityWithCsrfConfig.class, ...})
    2. public class CsrfEnabledIntegrationTest extends CsrfAbstractIntegrationTest {
    3. @Test
    4. public void givenNoCsrf_whenAddFoo_thenForbidden() throws Exception {
    5. mvc.perform(
    6. post("/foos").contentType(MediaType.APPLICATION_JSON)
    7. .content(createFoo())
    8. .with(testUser())
    9. ).andExpect(status().isForbidden());
    10. }
    11. @Test
    12. public void givenCsrf_whenAddFoo_thenCreated() throws Exception {
    13. mvc.perform(
    14. post("/foos").contentType(MediaType.APPLICATION_JSON)
    15. .content(createFoo())
    16. .with(testUser()).with(csrf())
    17. ).andExpect(status().isCreated());
    18. }
    19. }

    我们可以看到此测试如何使用不同的安全配置 — 启用了 CSRF 保护的配置。

    现在,如果不包含CSRF令牌,POST请求将失败,这当然意味着早期的攻击不再是一种选择。

    此外,测试中的csrf() 方法创建一个RequestPostProcessor,该处理器会自动在请求中填充有效的 CSRF 令牌以进行测试。

    7. 结论

    在本文中,我们讨论了几种CSRF攻击以及如何使用Spring Security来防止它们。

    与往常一样,本文中介绍的代码可在GitHub 上找到。

  • 相关阅读:
    GitModel 假设检验下|Task02多元数值向量的假设检验
    喜报|云畅科技获批加入全国信标委软件与系统工程分委会
    基于RetinaFace的口罩人脸检测算法
    java计算机毕业设计桔子酒店客房管理系统源程序+mysql+系统+lw文档+远程调试
    8万字208道Java经典面试题总结(附答案)
    【老生谈算法】matlab实现语音信号处理与仿真——语音信号处理算法
    企业级存储详解与存储资源盘活
    【1++的Linux】之文件(三)
    撰写高质量科研学术论文(写作方法与注意事项)
    SveletJs学习——数据绑定
  • 原文地址:https://blog.csdn.net/allway2/article/details/127706027