CSRF With Stateless REST API – 使用无状态REST API的CSRF

最后修改: 2021年 11月 12日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In our previous article, we’ve explained how CSRF attacks impact a Spring MVC application.

在我们的前一篇文章中,我们已经解释了CSRF攻击是如何影响Spring MVC应用程序的。

This article will go through different cases to determine if a stateless REST API can be vulnerable to CSRF attacks and, if so, how to protect it from them.

本文将通过不同的案例来确定无状态REST API是否会受到CSRF攻击,如果是的话,如何保护它不受攻击。

2. Does REST API Require CSRF Protection?

2.REST API是否需要CSRF保护?

First, we can find an example of a CSRF attack in our dedicated guide.

首先,我们可以在我们的专用指南中找到一个CSRF攻击的例子。

Now, upon reading this guide, we may think that a stateless REST API wouldn’t be affected by this kind of attack, as there’s no session to steal on the server-side.

现在,在阅读本指南时,我们可能认为无状态的REST API不会受到这种攻击的影响,因为在服务器端没有会话可以窃取。

Let’s take a typical example: a Spring REST API application and a Javascript client. The client uses a secure token as credentials (such as JSESSIONID or JWT), which the REST API issues after a user successfully signs in.

让我们举一个典型的例子:一个Spring REST API应用程序和一个Javascript客户端。客户端使用安全令牌作为凭证(如JSESSIONID或JWT),在用户成功登录后,REST API会发出该令牌。

CSRF vulnerability depends on how the client stores and sends these credentials to the API.

CSRF漏洞取决于客户端如何存储和发送这些凭证给API

Let’s review the different options and how they will impact our application vulnerability.

让我们回顾一下不同的选项以及它们将如何影响我们的应用漏洞。

We will take a typical example: a Spring REST API application and a Javascript client. The client uses a secure token as credentials (such as JSESSIONID or JWT), which the REST API issues after a user successfully signs in.

我们将举一个典型的例子:一个Spring REST API应用程序和一个Javascript客户端。客户端使用安全令牌作为凭证(如JSESSIONID或JWT),在用户成功登录后,REST API会发出该令牌。

2.1. Credentials Are Not Persisted

2.1.凭证未被持久化

Once we’ve retrieved the token from the REST API, we can set the token as a JavaScript global variable. This will save the token in the browser’s memory, and it will be available only for the current page.

一旦我们从REST API中获取了令牌,我们就可以将令牌设置为JavaScript全局变量。这将把令牌保存在浏览器的内存中,它将只对当前页面可用。

It’s the most secure way: CSRF and XSS attacks always lead to opening the client application on a new page, which can’t access the memory of the initial page used to sign in.

这是最安全的方式。CSRF和XSS攻击总是导致在一个新的页面上打开客户端应用程序,它不能访问用于登录的初始页面的内存。

However, our user will have to sign in again every time he accesses or refreshes the page.

然而,我们的用户在每次访问或刷新页面时,都必须再次登录。

On mobile browsers, it will happen even if the browser goes background, as the system clears the memory.

在移动浏览器上,即使浏览器进入后台,也会发生这种情况,因为系统会清除内存。

This is so restricting for the user that this option is rarely implemented.

这对用户的限制很大,所以这个选项很少被实施

2.2. Credentials Stored in the Browser Storage

2.2.存储在浏览器存储器中的凭证

We can persist our token in the browser storage – the session storage, for example. Then, our JavaScript client can read the token from it and send an authorization header with this token in all the REST requests.

我们可以将我们的令牌持久化在浏览器的存储中–比如说会话存储。然后,我们的JavaScript客户端可以从中读取令牌,并在所有的REST请求中发送一个带有该令牌的授权头。

This is a prevalent way to use, for example, JWT: it’s easy to implement and prevents attackers from using CSRF attacks. Indeed, unlike cookies, the browser storage variables are not sent automatically to the server.

这是一种普遍的使用方式,例如,JWT:它很容易实现,并防止攻击者使用CSRF攻击。事实上,与cookies不同,浏览器的存储变量不会自动发送到服务器。

However, this implementation is vulnerable to XSS attacks: a malicious JavaScript code can access the browser storage and send the token along with the request. In this case, we must protect our application.

然而,这种实现容易受到XSS攻击:恶意的JavaScript代码可以访问浏览器的存储,并将令牌与请求一起发送。在这种情况下,我们必须保护我们的应用程序

2.3. Credentials Stored in Cookies

2.3.存储在Cookies中的凭证

Another option is to use a cookie to persist the credentials. Then, the vulnerability of our application depends on how our application uses the cookie.

另一个选择是使用一个cookie来保存凭证。那么,我们的应用程序的脆弱性就取决于我们的应用程序如何使用cookie。

We can use a cookie to persist the credentials only, like a JWT, but not to authenticate the user.

我们可以像JWT一样,用cookie来保存证书,但不能用来验证用户。

Our JavaScript client will have to read the token and send it to the API in the authorization header.

我们的JavaScript客户端将不得不读取令牌,并将其发送到授权头中的API。

In this case, our application is not vulnerable to CSRF: Even if the cookie is sent automatically across a malicious request, our REST API will read credentials from the authorization header and not from the cookie. However, the HTTP-only flag must be turned to false to let our client read the cookie.

在这种情况下,我们的应用程序不会受到CSRF的攻击:即使cookie在恶意请求中被自动发送,我们的REST API将从授权头而不是cookie中读取证书。然而,HTTP-only标志必须转为false,以便让我们的客户端读取cookie。

However, by doing this, our application will be vulnerable to XSS attacks like in the previous section.

然而,通过这样做,我们的应用程序将容易受到XSS攻击,就像上一节所说的。

An alternative approach is to authenticate the requests from a session cookie, with the HTTP-only flag set to true. This is typically what Spring Security provides with the JSESSIONID cookie. Of course, to keep our API stateless, we must never use the session on the server-side.

另一种方法是通过会话cookie来验证请求,将HTTP-only标志设置为true。这就是Spring Security提供的JESSIONID cookie的典型做法。当然,为了保持我们的API无状态,我们决不能在服务器端使用会话。

In this case, our application is vulnerable to CSRF like a stateful application: As the cookie will be sent automatically with any REST requests, a click on a malicious link can perform authenticated operations.

在这种情况下,我们的应用程序就像一个有状态的应用程序一样容易受到CSRF的影响。由于cookie将随任何REST请求自动发送,点击恶意链接可以执行验证操作。

2.4. Other CSRF Vulnerable Configurations

2.4.其他有CSRF漏洞的配置

Some configurations don’t use secure tokens as credentials but may also be vulnerable to CSRF attacks.

有些配置不使用安全令牌作为凭证,但也可能容易受到CSRF攻击。

This is the case of HTTP basic authentication, HTTP digest authentication, and mTLS.

这就是HTTP基本认证HTTP摘要认证mTLS的情况。

They’re not very common but have the identical drawback: The browser sends credentials automatically on any HTTP requests. In these cases, we must enable CSRF protection.

它们不是很常见,但有相同的缺点。浏览器会在任何HTTP请求中自动发送证书。在这些情况下,我们必须启用CSRF保护。

3. Disable CSRF Protection in Spring Boot

3.禁用Spring Boot中的CSRF保护

Spring Security enables CSRF protection by default since version 4.

自版本4以来,Spring Security默认启用CSRF保护。

If our project doesn’t require it, we can disable it in a custom WebSecurityConfigurerAdapter:

如果我们的项目不需要它,我们可以在一个自定义的WebSecurityConfigurerAdapter中禁用它。

@Configuration
public class SpringBootSecurityConfiguration 
  extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
    }
}

4. Enable CSRF Protection With REST API

4.用REST API启用CSRF保护

4.1. Spring Configuration

4.1.Spring配置

If our project requires CSRF protection, we can send the CSRF token with a cookie by using CookieCsrfTokenRepository in a custom WebSecurityConfigurerAdapter.

如果我们的项目需要CSRF保护,我们可以通过在自定义WebSecurityConfigurerAdapter中使用CookieCsrfTokenRepository,将CSRF令牌与cookie一起发送。

We must set the HTTP-only flag to false to be able to retrieve it from our JavaScript client:

我们必须将HTTP-only标志设置为false,以便能够从我们的JavaScript客户端检索它。

@Configuration
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    }
}

After restarting the app, our requests receive HTTP errors, which means that CSRF protection is enabled.

重新启动应用程序后,我们的请求收到了HTTP错误,这意味着CSRF保护已经启用。

We can confirm that these errors are issued from the CsrfFilter class by adjusting the log level to DEBUG:

我们可以通过将日志级别调整为DEBUG来确认这些错误是从CsrfFilter类发出的。

<logger name="org.springframework.security.web.csrf" level="DEBUG" />

It will display:

它将显示。

Invalid CSRF token found for http://...

Also, we should see in our browser that a new XSRF-TOKEN cookie is present.

此外,我们应该在浏览器中看到一个新的XSRF-TOKENcookie存在。

Let’s add a couple of lines in our REST controller to also write the information to our API logs:

让我们在REST控制器中添加几行,把信息写到我们的API日志中。

CsrfToken token = (CsrfToken) request.getAttribute("_csrf");
LOGGER.info("{}={}", token.getHeaderName(), token.getToken());

4.2. Client Configuration

4.2.客户端配置

In the client-side application, the XSRF-TOKEN cookie is set after the first API access. We can retrieve it using a JavaScript regex:

在客户端应用程序中,XSRF-TOKEN cookie是在第一次API访问后设置的。我们可以用一个JavaScript的regex来检索它。

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

Then, we must send the token to every REST request that modifies the API state: POST, PUT, DELETE, and PATCH.

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

Spring is expecting to receive it in the X-XSRF-TOKEN header. We can simply set it with the JavaScript Fetch API:

Spring期望在X-XSRF-TOKEN头中收到它。我们可以简单地用JavaScript Fetch API来设置它。

fetch(url, {
    method: 'POST',
    body: JSON.stringify({ /* data to send */ }),
    headers: { 'X-XSRF-TOKEN': csrfToken },
})

Now, we can see that our request is working, and the “Invalid CSRF token” error is gone in the REST API logs.

现在,我们可以看到我们的请求正在工作,REST API日志中的“Invalid CSRF token”错误已经消失。

Therefore, it will be impossible for attackers to perform a CSRF attack. For example, a script that tries to perform the same request from a scam website will receive the “Invalid CSRF token” error.

因此,攻击者将无法进行CSRF攻击。例如,试图从一个诈骗网站执行相同请求的脚本将收到“无效的CSRF标记”错误。

Indeed, if the user hasn’t visited the actual website first, the cookie will not be set, and the request will fail.

事实上,如果用户没有先访问过实际的网站,cookie就不会被设置,请求也会失败。

5. Conclusion

5.总结

In this article, we reviewed the different contexts in which CSRF attacks against a REST API are possible or not.

在这篇文章中,我们回顾了针对REST API的CSRF攻击可能或不可能的不同情况。

Then, we learned how to enable or disable CSRF protection using Spring Security.

然后,我们学习了如何使用Spring Security启用或禁用CSRF保护。