Go Web应用程序中的CSRF(跨站请求伪造)

huangapple go评论78阅读模式
英文:

CSRF in Go Web Applications

问题

我想在我的Go Web应用程序中实现CSRF防护。用户不登录,但会填写表单并通过Stripe Checkout进行支付。

提交表单会在会话变量(cookie)中设置一个键,以便他们以后可以编辑自己发布的内容,并且在电子邮件中的URL允许他们在cookie过期后返回并再次进行编辑(如果需要)。

从我所看到的,我可以使用https://code.google.com/p/xsrftoken/和“双重提交cookie”方法来实现CSRF防护,具体步骤如下:

  • 根据任意用户ID(使用uuid.V4()通过go-uuid生成)生成一个CSRF令牌,例如:

      if session.Values["id"] == "" {
          session.Values["id"] = uuid.NewV4()
      }
    
      csrfToken := xsrftoken.Generate(csrfKey, session.Values["id"], "/listing/new/post")
    
  • ...将其存储在会话中,并在模板中的隐藏字段中呈现:

      session.Values["csrfToken"] = csrfToken
      ...
      <input type="hidden" id="_csrf" value={{ .csrfToken }}>
    
  • 当用户提交表单时,我需要获取我生成的ID,确认表单中提交的csrfToken与会话中的令牌是否匹配,如果匹配,则使用xsrftoken包验证其是否过期:

      userID := session.Values["id"]
    
      if session.Values["csrfToken"] != r.PostFormValue("csrfToken") {
          http.Redirect(w, r, "/listing/new", 400)
      }
    
      if !xsrftoken.Valid(session.Values["csrfToken"], csrfKey, userID, "/listing/new/post") {
          http.Redirect(w, r, "/listing/new", 400)
      }
    

我关注的问题是:

  • 每次呈现表单时,我应该生成一个新的令牌吗?还是可以重用一个未过期的令牌用于单个用户会话? 更新:根据这个答案,我应该只在每个会话中生成一个新的令牌(即同一用户在同一表单上获得相同的令牌,直到令牌过期)。

  • 鉴于更新后的问题,我如何处理在用户请求表单和提交表单之间令牌过期的情况?(例如,令牌还剩10分钟,他们暂时离开了一会儿)将他们重定向回表单(当然要重新填充!)并生成新的会话ID + CSRF令牌?

  • 是否有其他方法可以实现这一点?Coding Horror指出,SO为发送给客户端的每个HTML表单生成一个唯一键。如果我想使用xsrftoken包采用这种方法,我该如何操作,考虑到它在生成新键时需要一个userID?

  • 我还忽略了什么其他问题吗?

英文:

I want to implement CSRF prevention in my Go web application. Users don't log in, but they do fill out forms and pay (via Stripe Checkout).

Posting something sets a key in a session variable (cookie) so they can later edit what they've posted, and a URL in an email allows them to come back when the cookie has expired and edit it again if need be.

From what I can see, I can use https://code.google.com/p/xsrftoken/ with the "double submitted cookie" method to implement CSRF prevention by:

  • Generate a CSRF token against an arbitrary user ID (uuid.V4() via go-uuid), like so:

      if session.Values[&quot;id&quot;] == &quot;&quot; {
      session.Values[&quot;id&quot;] = uuid.NewV4()
      }
    
      csrfToken := xsrftoken.Generate(csrfKey, session.Values[&quot;id&quot;], &quot;/listing/new/post&quot;)
    
  • ... and store that in the session and render it in a hidden field in the template:

      session.Values[&quot;csrfToken&quot;] = csrfToken
      ...
      &lt;input type=&quot;hidden&quot; id=&quot;_csrf&quot; value={{ .csrfToken }}&gt;
    
  • When the user submits the form, I need to get the ID I generated, confirm that the submitted csrfToken from the form matches the one in the session, and if so, validate it with the xsrf package to confirm it hasn't expired:

      userID := session.Values[&quot;id&quot;]
    
      if session.Values[&quot;csrfToken&quot;] != r.PostFormValue(&quot;csrfToken&quot;) {
      http.Redirect(w, r, &quot;/listing/new&quot;, 400)
      }
    
      if !xsrftoken.Valid(session.Values[&quot;csrfToken&quot;], csrfKey, userID, &quot;/listing/new/post&quot;) {
      http.Redirect(w, r, &quot;/listing/new&quot;, 400)
      }
    

My pertinent questions are:

  • Should I generate a new token every time the form is rendered? Or is it acceptable to re-use a non-expired token for a single user session? Update: According to this answer I should only generate a new token per session (i.e. so the same user gets the same token on the same form, until the token expires)

  • Given the updated question, how do I handle the situation where a created token expires between the time the user requests the form and then submits the form? (perhaps it had 10 minutes left, and they alt+tabbed out for a while) Re-direct them back to the form (re-populated, of course!) and generate a new session id + csrf token?

  • Is there a different way to do this? Coding Horror indicates that SO generates a unique key for every HTML form sent to the client? How would I go about going down this route with the xsrf package, given that it wants a userID when generating a new key?

  • What else have I overlooked?

答案1

得分: 6

每次渲染表单时都生成一个新的令牌是一个好主意。给定一个持久的攻击者和一个可行的入口向量,只是时间的问题,直到攻击者获得两者。然而,如果在攻击者能够破解当前令牌之前,至少有一个标识符重新生成,那就没有问题。

对于在用户请求表单和提交表单之间令牌过期的情况,你可以通过AJAX更新cookie和CSRF令牌,以便为客户端提供更长的时间来填写表单。

如果你想要为每个发送给客户端的HTML表单生成一个唯一的密钥,可以考虑使用xsrf包。不过,由于xsrf包在生成新密钥时需要一个用户ID,你需要找到一种方法来为每个用户生成唯一的ID。

英文:

> Should I generate a new token every time the form is rendered? Or is
> it acceptable to re-use a non-expired token for a single user session?
> Update: According to this answer I should only generate a new token
> per session (i.e. so the same user gets the same token on the same
> form, until the token expires)

It is a good idea to regenerate both the token and session ID often. Given a persistent attacker and a viable entry vector, it's just a matter of time until the attacker obtains both. If, however, at least one of both identifiers regenerates before the attacker is able to crack the current one, then no problem.

> Given the updated question, how do I handle the situation where a
> created token expires between the time the user requests the form and
> then submits the form? (perhaps it had 10 minutes left, and they
> alt+tabbed out for a while) Re-direct them back to the form
> (re-populated, of course!) and generate a new session id + csrf token?

You can update cookies and CSRF tokens through AJAX if you want to give your client vast time to fill out a form.

> Is there a different way to do this? Coding Horror indicates that SO
> generates a unique key for every HTML form sent to the client? How
> would I go about going down this route with the xsrf package, given
> that it wants a userID when generating a new key?

The more tightly bound a token is to a certain action that requires authentication, the more fine-grained control you have. If you can uniquely identify each form in your application then I'd say do it.

答案2

得分: 4

我为Go语言创建了一个名为nosurf的CSRF保护包。以下是它处理你提到的几个方面的方式:

  • 令牌是通过从CS PRNG获取字节并使用base64进行编码来创建的。它不会在每次页面加载或每个表单中重新生成,但可以通过用户可调用的函数重新生成令牌。
  • 然后将令牌存储在一个cookie中(不是会话,因为它是一个通用的中间件,不仅适用于特定的框架)。该cookie的有效期为一年,但你可以轻松修改此持续时间。
  • nosurf负责取消请求,并返回403或调用自定义的失败处理程序(如果设置了)。你不需要在代码中编写if CsrfCheckOk(r) { ... }或类似的内容。
  • 不幸的是,它无法解决页面加载和表单提交之间令牌过期的问题。

这就是它的工作方式,尽管我不确定这是否是处理CSRF的最佳方式。针对特定框架的包可能在某些方面由于紧密集成而处理得更好。

英文:

I've created a CSRF protection package for Go called nosurf. Here's how it handles the areas you mentioned:

  • Token is created by taking bytes from CS PRNG and encoding them using base64. It is not regenerated for every page load or every form, though there is a user-callable function for regenerating the token.
  • It is then stored in a cookie (not a session, as it's a generic middleware not intended for any specific framework only). The cookie lasts one year, but you can easily modify this duration.
  • nosurf takes care of cancelling the request and either returning 403 or calling your custom failure handler (if set). You don't have to have if CsrfCheckOk(r) { ... } or anything like that in your code.
  • Sadly, it doesn't address token expiring inbetween the page load and the form submission.

So that's it, even though I'm not sure it is the best way to handle CSRF all-around. A package for a specific framework might handle it better in some ways due to tight integration.

huangapple
  • 本文由 发表于 2013年10月2日 12:24:35
  • 转载请务必保留本文链接:https://go.coder-hub.com/19129985.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定