无需令牌或隐藏表单字段的 CSRF 保护
CSRF protection without tokens or hidden form fields

原始链接: https://blog.miguelgrinberg.com/post/csrf-protection-without-tokens-or-hidden-form-fields

## Microdot 中的 CSRF 保护现代化 最近,作者为他们的 Web 框架 Microdot 添加了 CSRF(跨站请求伪造)保护。 最初计划实现传统的反 CSRF 令牌方法,但他们发现一种更新、更简单的方法在 Go 和 Ruby 社区中越来越受欢迎。 这种“现代”方法利用了现代浏览器自动包含的 `Sec-Fetch-Site` HTTP 标头。 该标头可靠地指示请求是否来自同一站点,从而防止恶意跨站点请求。 作者实施了此方法,并添加了选项来控制子域处理以及使用 `Origin` 标头作为后备机制,以支持缺乏 `Sec-Fetch-Site` 支持的旧版浏览器。 有趣的是,这种方法尚未获得 OWASP(开放 Web 应用程序安全项目)的完全认可,目前列为“纵深防御”而不是完整解决方案——尽管社区讨论呼吁将其提升。 尽管如此,作者认为这对 Microdot 来说是一项重大改进,符合其简约设计。 该实现是开源的,可供审查,并会持续监控 OWASP 的指导,以便进行潜在的未来更新,包括在需要时基于令牌的后备方案。

这个Hacker News讨论围绕CSRF(跨站请求伪造)保护方法。最初的帖子强调了一种避免传统CSRF令牌或隐藏表单字段的技术,而是依赖于像`Sec-Fetch-Site`这样的HTTP头部。但作者澄清了一个错误:OWASP最近更新了其指导,将Fetch Metadata定位为顶级的CSRF保护,此前曾短暂将其降级为“纵深防御”。 对话随后分化到替代和补充方法。几位评论者提倡`SameSite` cookie属性(“Strict”或“Lax”)作为一种更现代、更简单的解决方案,尽管OWASP目前将其视为二级防御。有人担心`SameSite=Strict`可能会在使用外部链接导航时将用户注销。 讨论还涉及基于头部保护对复杂XSS攻击的局限性,并强调CSRF主要解决*没有*访问身份验证令牌的攻击,利用浏览器cookie处理。最终,该帖子强调了网络安全不断变化的环境以及关于最佳CSRF缓解策略的持续争论。
相关文章

原文

A couple of months ago, I received a request from a random Internet user to add CSRF protection to my little web framework Microdot, and I thought it was a fantastic idea.

When I set off to do this work in early November I expected I was going to have to deal with anti-CSRF tokens, double-submit cookies and hidden form fields, pretty much the traditional elements that we have used to build a defense against CSRF for years. And I did start along this tedious route. But then I bumped into a new way some people are dealing with CSRF attacks that is way simpler, which I describe below.

Implementing a security feature

An often shared piece of advice is that you should never implement security features yourself. Instead, you should look for well established solutions built by people who think about security day in and day out.

Unfortunately, as the lead (and only) maintainer of Microdot, I do not have an ecosystem of existing solutions available to me. Even though I gladly accept external contributions, most of the framework has been built by myself out of nothing. So in this case, like many other times before, I felt I had no choice but to go against the standard advice and write CSRF protection code by myself, because if I didn't do it then the feature would not be built.

What is the first step when you need to build a security feature? Check out what OWASP has to say about the matter.

So, in early November, I opened OWASP's CSRF Prevention Cheat Sheet page to see what was new and interesting in the world of CSRF protection. And I found that nothing of significance had changed.

According to OWASP, the best CSRF protection you could get (at the time I checked) was still built around the idea of using anti-CSRF tokens. So I set off to implement this for Microdot.

A disturbance in the (CSRF) force

I was happily making progress on my CSRF implementation, and then in early December, another random Internet user dropped an issue on the Flask repository, proposing that Flask adds support for "modern" CSRF protection. Modern? How could there be a new way to protect against CSRF that isn't mentioned by OWASP?

This led me down a rabbit hole of blog posts and discussions spanning the Go and Ruby communities, plus a long discussion about this method on the OWASP GitHub repository itself, resulting in a pull request that added a mention of this method to the CSRF Cheat Sheet, only a couple of weeks after I went to this page looking for guidance for my own implementation.

Modern CSRF Protection

The so called "modern" method to protect against CSRF attacks is based on the Sec-Fetch-Site header, which all modern desktop and mobile browsers include in the requests they send to servers. According to Mozilla, all browsers released since March 2023 have support for this header.

The Sec-Fetch-Site header can have one of four values:

  • same-origin, when the request comes from the same origin as the target server
  • same-site, when the request comes from the same site, but not exactly the same origin (e.g. a different subdomain) as the target server
  • cross-site, when the request comes from an origin that does not match the target server
  • none, when the request is originated by the user

The value of this header cannot be set via JavaScript, so the server can assume that a) if this header is present, then the client is a web browser, and b) the value of the header can be trusted. So basically, the server can reject requests that come with this header set to cross-site, and in essence that is all you need to do to protect against CSRF!

After seeing this, I paused my work on the token-based CSRF implementation and spent a few hours to implement this modern approach. As always, the devil is in the details, so let's see what else I needed to do to build a complete solution.

First of all, in some cases subdomains sharing the same registered domain may operate independently, and as such, it is not out of the question that one subdomain may attempt to attack another through CSRF. Depending on the level of trust an application has for other subdomains, a server may want to block requests that come with the Sec-Fetch-Site header set to same-site. In Microdot, I have added an argument allow_subdomains to cover this case. I decided to err on the side of security, so the default is False, meaning that requests from subdomains are also blocked.

The other big problem is that not everyone is using a recent browser that implements this header. Looking at the browser compatibility for the Sec-Fetch-Site header, you can see that most browsers implemented this feature long ago, between 2019 and 2021, with one notable exception: Safari. Apple added this header to its browser in 2023, so it is reasonable to assume that there are still users out there running older browsers that do not support it.

One option is to reject all requests that do not have the Sec-Fetch-Site header. This keeps everyone secure, but of course, there's going to be some unhappy users of old devices that will not be able to use your application. Plus, this would also reject HTTP clients that are not browsers. If this is not a problem for your use case, then great, but it isn't a good solution overall.

From what I gathered from looking at other implementations of this method, an accepted solution is to use the Origin header as fallback when Sec-Fetch-Site is not implemented, since this header has been around for much longer. The last of the major browsers to add it were Firefox desktop in 2019 and Edge and Firefox mobile in 2020. Like Sec-Fetch-Site, the Origin header is also a restricted header that is set by the browser, so it can also be used to determine from where a request is coming from.

The problem with using the Origin header is that it isn't always easy to know what is the correct origin that applies to a web application. The standard option is to compare the value of the Origin header against the value of the Host header, but Host only includes the hostname and port, while Origin also includes the scheme. Also, the Host header is overwritten as it passes through reverse proxies. So comparing these two headers is actually not easy.

Another, more direct option is to ask the user to configure the expected origin name explicitly. To keep things simple, in Microdot I opted for the explicit configuration, for which I linked to the existing Cross-Origin Request Sharing (CORS) support. The CORS feature already maintains a list of allowed origins, so my CSRF logic automatically trusts these. I decided to not complicate myself adding support for Host header checks at this time, but maybe I'll add this in the future.

Filippo Valsorda, a security developer active in the Go ecosystem (and author of the popular mkcert tool) wrote a blog post about this method that you may want to check out if you want to learn more details about it. He seems to be the first to propose this method and has implemented it for the Go standard library.

Also if you are interested, feel free to review my implementation of CSRF protection in Microdot. Have a look at the documentation, the code and an example, and let me know if you have any improvements or fixes to suggest.

Let's revisit OWASP

As I mentioned above, the CSRF Prevention Cheat Sheet page from OWASP was updated in early December to include the use of the Sec-Fetch-Site header in the list of prevention methods. But this method is currently listed as a defense in depth mechanism, and not a complete solution, which I thought was odd.

I referenced the discussion in the OWASP GitHub repository that resulted in the recent changes made to the Cheat Sheet page. Several participants in that discussion have suggested that this method should be upgraded to a complete alternative to the standard token-based approaches. The OWASP maintainer was initially skeptical, but towards the end of the thread they have agreed. The pull request that closed the discussion added this solution as an alternative to the token-based approaches, but then a later change made significant updates, including the downgrade to defense in depth. My hope is that this is just a misunderstanding, and that the OWASP folks will restore the content as it was agreed by all the parties involved.

In any case, I consider that in Microdot, going from no CSRF support at all to this is a great step forward that is also consistent with the minimalist ethos of the project. I will be keeping an eye on the OWASP CSRF Cheat Sheet page to see what is their final word on this new protection method, and if they end up keeping it as defense in depth, I still have a mostly complete implementation of double-submit anti-CSRF tokens that I can bring into my project.

Conclusion

What I like the most about working in open source is that all the work happens in the open, so it is a permanent record that can be searched and reviewed. My CSRF protection journey started as a somewhat tedious exercise in the use of cryptography and cookies, but then thanks to an unexpected lead it turned into a fun and exciting learning opportunity for me.

Thank you for visiting my blog! If you enjoyed this article, please consider supporting my work and keeping me caffeinated with a small one-time donation through Buy me a coffee. Thanks!

联系我们 contact @ memedata.com