如何在 RESTful 应用程序中防止 CSRF?
通常使用以下方法之一来防止跨站请求伪造 (CSRF):
- 检查引用者 - REST 风格但不可靠
- 将令牌插入表单并将令牌存储在服务器会话中 - 不是真正的 REST 风格
- 神秘的一次性 URI - 出于同样的原因不是 REST 风格因为令牌
- 为此请求手动发送密码(不是与 HTTP 身份验证一起使用的缓存密码) - RESTful 但不方便
我的想法是使用用户秘密、神秘但静态的表单 id 和 JavaScript 来生成令牌。
<form method="POST" action="/someresource" id="7099879082361234103">
<input type="hidden" name="token" value="generateToken(...)">
...
</form>
GET /usersecret/john_doe
由 JavaScript 从经过身份验证的用户获取。- 响应:
OK 89070135420357234586534346
此秘密在概念上是静态的,但可以每天/每小时更改......以提高安全性。这是唯一的机密。 - 使用 JavaScript 读取神秘的(但对所有用户来说都是静态的!)表单 ID,将其与用户密钥一起处理:
generateToken(7099879082361234103, 89070135420357234586534346)
- 将表单与生成的令牌一起发送到服务器。
- 由于服务器知道用户密码和表单 ID,因此可以在发送之前运行与客户端相同的generateToken 函数并比较两个结果。只有当两个值相等时,该操作才会被授权。
尽管事实上没有 JavaScript 就无法工作,但这种方法有什么问题吗?
附录:
Cross Site Request Forgery (CSRF) is typically prevent with one of the following methods:
- Check referer - RESTful but unreliable
- insert token into form and store the token in the server session - not really RESTful
- cryptic one time URIs - not RESTful for the same reason as tokens
- send password manually for this request (not the cached password used with HTTP auth) - RESTful but not convenient
My idea is to use a user secret, a cryptic but static form id and JavaScript to generate tokens.
<form method="POST" action="/someresource" id="7099879082361234103">
<input type="hidden" name="token" value="generateToken(...)">
...
</form>
GET /usersecret/john_doe
fetched by the JavaScript from the authenticated user.- Response:
OK 89070135420357234586534346
This secret is conceptionally static, but can be changed every day/hour ... to improve security. This is the only confidential thing. - Read the cryptic (but static for all users!) form id with JavaScript, process it together with the user secret:
generateToken(7099879082361234103, 89070135420357234586534346)
- Send the form along with the generated token to the server.
- Since the server knows the user secret and the form id, it is possible to run the same generateToken function as the client did before sending and compare both results. Only when both values are equal the action will be authorized.
Is something wrong with this approach, despite the fact that it doesn't work without JavaScript?
Addendum:
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
这里有很多答案,其中也有不少问题。
你不应该做的事情:
如果你需要从 JavaScript 读取会话令牌,那么你就犯了严重错误。您的会话标识符 cookie 应始终设置 HTTPOnly,以便脚本无法使用它。
这一保护可以大大减少 XSS 的影响,因为攻击者将无法再获取登录用户的会话令牌,而会话令牌在所有意图和目的上都相当于应用程序中的凭据。您不希望因为一个错误而获得王国的钥匙。
会话标识符不应写入页面内容。这与您设置 HTTPOnly 的原因相同。这意味着您的 csrf 令牌不能是您的会话 ID。它们需要具有不同的值。
您应该做的事情:
遵循OWASP 指南:
具体来说,如果这是 REST 应用程序,您可以要求双重提交 CSRF 令牌。如果您这样做,请确保将其定义为特定的完整域 (www.mydomain.com)而不是父域 (example.com),并且您还使用越来越受欢迎的“samesite”cookie 属性。
只需创建一些加密随机的内容,将其存储为 ASCII Hex 或 Base64 编码,并在服务器返回页面时将其作为 cookie 添加到您的表单中。在服务器端确保 cookie 值与表单值匹配。瞧,您已经杀死了 CSRF,避免了对用户的额外提示,并且没有让自己面临更多漏洞。
注意:正如 @krubo 在下面所述,双重提交技术 已被发现有一些弱点(参见双重提交)。由于此弱点要求:
我认为该弱点更多地属于“Cool Defcon Talk”类别,而不是“现实世界安全风险”类别。无论如何,如果您打算使用双重提交,那么采取一些额外的步骤来充分保护自己并没有什么坏处。
新更新 07/06/2020
我最喜欢的双重提交新方法是像以前一样在请求正文中创建并传递加密随机字符串;但不是让 cookie 具有相同的精确值,而是让 cookie 成为由证书签名的字符串的编码值。这在服务器端仍然很容易验证,但攻击者很难模仿。您仍然应该使用我之前在帖子中概述的相同站点 Cookie 属性和其他保护措施。
There are a lot of answers here, and problems with quite a few of them.
Things you should NOT do:
If you need to read the session token from JavaScript, you're doing something horribly wrong. Your session identifier cookie should ALWAYS have HTTPOnly set on it so its not available to scripts.
This one protection makes it so that the impact of XSS is considerably reduced, since an attacker will no longer be able to get a logged in users session token, which for all intents and purposes are the equivalent of credentials in the application. You don't want one error to give keys to the kingdom.
The session identifier should not be written to the contents of the page. This is for the same reasons you set HTTPOnly. This means that that your csrf token can not be your session id. They need to be different values.
Things you should do:
Follow OWASP's guidance:
Specifically, if this is a REST application you can require double-submission of CSRF tokens. If you do this, just be sure that you define it to a specific full-domain (www.mydomain.com) and not a parent domain (example.com), and that you also utilize the "samesite" cookie attribute which is gaining popularity.
Simply create something cryptographically random, store it in ASCII Hex or Base64 encode, and add it as a cookie and to your forms when the server returns the page. On the server side make sure that the cookie value matches the form value. Voila, you've killed CSRF, avoided extra prompts for your users, and not opened yourself up to more vulnerabilities.
NOTE: As @krubo states below the double-submission technique has been found to have some weaknesses (See Double-Submission). Since this weakness requires that:
I kind of think the weakness falls more in the category of a "Cool Defcon Talk" rather than a "Realworld Security Risk". In any case, if you are going to use double-submission it doesn't hurt to take a few extra steps to protect yourself fully.
New Update 07/06/2020
My new favorite way to do double-submission is to create and pass a cryptographic random string in the body of the request as before; but rather than have the cookie be the same exact value have the cookie be the encoded value of the string being signed by a certificate. This is still just as easy to validate on the server side, but is MUCH harder for an attacker to mimic. You should still use the samesite Cookie attribute and other protections outlined earlier in my post.
我的理解是否正确:
那么,为什么不检查用户是否通过 cookie 登录并然后才应用 CSRF?
我不确定,但另一个网站是否有可能伪造基本身份验证或标头等内容?
据我所知,CSRF都是关于cookie的? Cookie 不会进行 RESTful 身份验证。
Am I getting this right:
So, why not check whether users is logged in via cookie and apply CSRF only then?
I'm not sure but is possible for another site to forge things like Basic auth or headers?
As far as I know , CSRF is all about cookies? RESTful auth doesn't happen with cookies.
您肯定需要服务器上的某些状态来进行身份验证/授权。但它不一定是 http 会话,您可以将其存储在分布式缓存(如 memcached)或数据库中。
<罢工>
如果使用cookie进行身份验证,最简单的解决方案是双重提交cookie值。在提交表单之前,从 cookie 中读取会话 ID,将其存储在隐藏字段中,然后提交。在服务器端,确认请求中的值与会话 ID(从 Cookie 中获取)相同。来自另一个域的恶意脚本将无法从 cookie 中读取会话 ID,从而防止 CSRF。
此方案在整个会话中使用单个标识符。
如果您想要更多保护,请为每个会话每个表单生成一个唯一的 ID。
另外,不要在 JS 中生成令牌。任何人都可以复制代码并从不同的域运行它来攻击您的网站。
You definitely need some state on the server to authenticate/authorize. It need not be the http session though, you could store it in a distributed cache (like memcached) or a database.
If you use cookies for authentication, the easiest solution is to double-submit the cookie value. Before you submit the form, read the session id from the cookie, store it in a hidden field and then submit it. On the server side, confirm that the value in the request is the same as the session id (that you got from the cookie). Evil script from another domain will not be able to read the session id from the cookie, thus preventing CSRF.
This scheme uses a single identifier across the session.
If you want more protection, generate a unique id per-session per-form.
Also, DO NOT generate tokens in JS. Anybody can copy the code and run it from a different domain to attack your site.
静态表单 ID 根本不提供任何保护;攻击者可以自己获取它。请记住,攻击者并不局限于在客户端上使用 JavaScript;他可以在服务器端获取静态表单 ID。
我不确定我是否完全理解拟议的辩护;
GET /usersecret/john_doe
来自哪里?那是页面 JavaScript 的一部分吗?这是实际建议的 URL 吗?如果是这样,我假设username
不是秘密,这意味着如果浏览器或插件错误允许跨域 GET 请求,evil.ru 可以恢复用户秘密。为什么不在身份验证时将用户机密存储在 cookie 中,而不是让任何可以执行跨域 GET 的人检索它?在实施之前,我会非常仔细地阅读“跨站伪造的稳健防御”我自己的身份验证系统,我希望能够抵抗 CSRF。事实上,我会重新考虑实施我自己的身份验证系统。
The static form ID provides no protection at all; an attacker can fetch it himself. Remember, the attacker is not constrained to using JavaScript on the client; he can fetch the static form ID server-side.
I'm not sure I entirely understand the proposed defense; where does the
GET /usersecret/john_doe
come from? Is that part of the page JavaScript? Is that the literal proposed URL? If so, I'm assuming thatusername
is not a secret, which means that evil.ru can recover user secrets if a browser or plugin bug allows cross-domain GET requests. Why not store the user secret in a cookie upon authentication rather than let anyone who can do cross-domain GETs retrieve it?I would read "Robust Defenses for Cross-Site Forgery" really carefully before I implemented my own authentication system that I wanted to be resistant to CSRF. In fact, I would reconsider implementing my own authentication system at all.
CSRF 预防备忘单中提供了一些可供以下人员使用的方法令人安心的服务。最 RESTful 无状态 CSRF 缓解措施是使用 Origin 或 HTTP 引荐来源< /a> 以确保请求来自您信任的域。
There are a few methods in the CSRF Prevention Cheat Sheet that can be used by restful service. The most RESTful stateless CSRF mitigation is using the Origin or HTTP referer to make sure the requests originate from a domain you trust.
如果您将其发送给客户端,您的用户秘密就不再是秘密。我们通常使用此类秘密来生成哈希值并将其与表单一起发送,然后等待它们返回进行比较。
如果您想要 RESTful,请求必须包含有关如何处理它的所有信息。您可以执行此操作的方法:
使用 REST 客户端添加 csrf 令牌 cookie,并使用表单在隐藏输入中发送相同的令牌。如果服务和客户端位于不同的域下,则必须共享凭据。在服务上,您必须比较 2 个令牌,如果它们相同,则请求有效...
您可以使用 REST 服务添加 csrf 令牌 cookie,并使用您的资源表示发送相同的令牌(隐藏输入等)。其他一切与上一个解决方案的结尾相同。该解决方案处于 RESTful 的边缘。 (在客户端不调用服务修改cookie之前都可以。如果cookie是http only,则客户端不应该知道它,如果不是,则客户端应该设置它。)您可以做更多如果您向每个表单添加不同的令牌并向 cookie 添加过期时间,则解决方案会很复杂。您也可以将过期时间与表单一起发送回来,这样您就会知道令牌验证失败的原因。
您可以在服务的资源状态中拥有用户密钥(每个用户都不同)。通过构建表示,您可以为每个表单生成一个令牌(和过期时间)。您可以根据实际令牌(以及过期时间、方法、url 等)和用户密钥生成哈希值,并将该哈希值与表单一起发送。当然,您对“用户秘密”保密,因此您永远不会将其与表单一起发送。之后,如果您的服务收到请求,您可以再次根据请求参数和用户密钥生成哈希,并比较它们。如果不匹配,则请求无效...
如果您的 REST 客户端是可注入 javascript 的,那么它们都不会保护您,因此您必须根据 HTML 实体检查所有用户内容,并删除所有内容,或者使用 TextNodes总是而不是innerHTML。您还必须保护自己免受 SQL 注入和 HTTP 标头注入。切勿使用简单的 FTP 来刷新您的站点。等等...有很多方法可以将邪恶代码注入您的网站...
我几乎忘记提及,GET 请求始终供服务和客户端读取。对于服务来说,这是显而易见的,客户端在浏览器中设置任何 url 都必须生成一个或多个资源的表示,它永远不应该对资源调用 POST/PUT/DELETE 方法。例如
GET http://my.client.com/resource/delete -> DELETE http://my.api.com/resource
是一个非常非常糟糕的解决方案。但如果你想阻碍 CSRF,这是非常基本的技能。Your user secret is not a secret if you send it to the client. We usually use such secrets to generate hashes and send them with the form, and wait them back for comparison.
If you want to be RESTful, the request has to contain every information about how to process it. The ways you can do this:
Add a csrf token cookie with your REST client and send the same token in hidden input with your forms. If the service and the client are under different domains, you have to share the credentials. On the service you have to compare the 2 tokens, and if they are the same, the request is valid...
You can add the csrf token cookie with your REST service and send the same token with the representations of your resources (hidden inputs, etc...). Everything else is the same as the end of the previous solution. This solution is on the edge of RESTfulness. (It is okay until the client do not call the service to modify the cookie. If the cookie is http only, the client should not know about it, if it is not, then the client should set it.) You can do a more complex solution if you add different tokens to each forms and add expiration time to the cookies. You can send the expiration time back with the forms as well, so you will know the reason when a token validation fails.
You can have a user secret (different by each user) in the resource state on you service. By building representations, you can generate a token (and expiration time) for each form. You can generate a hash from the actual token (and expiration time, method, url, etc...) and the user secret, and send that hash with the form as well. You keep the "user secret" in secret of course, so you never send that with the form. After that if your service gets a request, you can generate the hash from the request parameters and user secret again, and compare them. If the don't match, the request is invalid...
None of them will protect you if your REST client is javascript injectable, so you have to check all your user content against HTML entities, and remove all of them, or use TextNodes always instead of innerHTML. You have to protect yourself against SQL injection and HTTP header injection as well. Never use simple FTP to refresh your site. And so on... There are many ways to inject evil code into your site...
I almost forgot to mention, that GET requests are always for reading by the service and by the client either. By the service this is obvious, by the client setting any url in the browser must result a representation of a resource or multiple resources, it should never call a POST/PUT/DELETE method on a resource. For example
GET http://my.client.com/resource/delete -> DELETE http://my.api.com/resource
is a very-very bad solution. But this is very basic skill if you want to hinder CSRF.