Rails Rails 中消失的 CSRF token

warmwind · 2014年10月01日 · 最后由 hiveer 回复于 2018年10月24日 · 12292 次阅读

CSRF(Cross-Site Request Forgery) 是一种常见的攻击手段,Rails 中下面的代码帮助我们的应用来阻止 CSRF 攻击。

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception
end

这段代码是 Rails4 自动生成的,这里使用了with: :exception设置了对在handle_unverified_request使用的策略是抛出异常ActionController::InvalidAuthenticityToken。 Rails3 中默认使用的reset_session。 Rails 防止 CSRF 的机制是在表单中随机生成一个 authenticity_token,同时存储于表单的隐藏域以及当前的 session 中,当表单提交时,而 server 端就可以比较这两处的是否一致来做出判断,判断请求的来源是否可靠,因为第三方是无法知道 session 中的 token 的。

# Sets the token value for the current session.
def form_authenticity_token
  session[:_csrf_token] ||= SecureRandom.base64(32)
end
<div style="margin:0;padding:0;display:inline">
  <input name="utf8" type="hidden" value="✓">
  <input name="authenticity_token" type="hidden" value="EZWDs44j5vzY+DCsgTHL0iPYiOUwaFnemwtGmo2AVRM=">
</div>

当然,这些都是正常情况,当表单要作为 ajax 提交,也就是data-remote=true时,情况就不同了,默认配置下,authenticityt_token不再自动生成。如果是 Rails3 就会发现 session 中的信息不见了,如果是把 user_id 存储在 session 中的,当然登录的状态就改变了。如果是 Rails4,默认就会得到上面提到的InvalidAuthenticityToken异常。

#form_tag_helper.rb
def html_options_for_form(url_for_options, options)
   options.stringify_keys.tap do |html_options|
     ...
     if html_options["data-remote"] &&
        !embed_authenticity_token_in_remote_forms &&
        html_options["authenticity_token"].blank?
       # The authenticity token is taken from the meta tag in this case
       html_options["authenticity_token"] = false
     elsif html_options["authenticity_token"] == true
       # Include the default authenticity_token, which is only generated when its set to nil,
       # but we needed the true value to override the default of no authenticity_token on data-remote.
       html_options["authenticity_token"] = nil
     end
   end
 end

上面代码的 5-14 行可以看到生成 token 时的配置判断,从中也可以得到解决的两种办法: 1. 配置

config.action_view.embed_authenticity_token_in_remote_forms = true

2. 通过 JS 获取 其实在默认的 layout 中,一般会有一行<%= csrf_meta_tags %>,它的定义是:

def csrf_meta_tags
  if protect_against_forgery?
    [
      tag('meta', :name => 'csrf-param', :content => request_forgery_protection_token),
      tag('meta', :name => 'csrf-token', :content => form_authenticity_token)
    ].join("\n").html_safe
  end
end

它在页面的 head 中增加一个csrf-token的属性

meta content="authenticity_token" name="csrf-param" />
meta content="VY13wlC2rgGccbkxyvm7Z1WX4LKH+71vzIj+8Um0QO8=" name="csrf-token" />

这与表单渲染出的 authenticity_token 完全一致,所以这就给了我们通过 js 来给表单设置 authenticity_token 的办法,如下

//application.js
$('input[name=authenticity_token]').val($('meta[name=csrf-token]').attr('content'))

如果在所有 ajax 异步请求中,都提交 csrf-token,可以直接这样设置

$.ajaxSetup({ headers: { 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') } });

为什么当data-remote=true时,token要消失呢??

我们又要手动去传递token,这样不是很麻烦么?意义何在?

怎么印象中以前好像没这个问题

怪不得印象中没有 InvalidAuthenticityToken 这个问题。

看了 http://www.alfajango.com/blog/rails-4-whats-new/

form 加了 remote: true 时,默认是不会有 token 存在,不过当提交时 rails.js 会自动补上这个token

为什么 authenticity_token 这个值各个页面都是同一个值?

@dorayatou 不会是一个值啊,每次刷新 Rails 都会自动再次生成

@warmwind 我有一个疑问,当打开多个页面的时候,rails 会生成不同的 authenticity_token,那么不同页面的 authenticity_token 不同,当 rails 验证的时候会用最后产生的 authenticity_token 验证,还是每个页面的验证都是没有问题的?

@warmwind 感觉你中间有一句话说得不够明确,会导致误解:

“而 server 端就可以比较这两处的是否一致来做出判断,判断请求的来源是否可靠,”「因为第三方是无法知道 session 中的 token 的」

主要是最后一句「因为第三方是无法知道 session 中的 token 的」。 第三方在这里应该指的是另外一个站点。这句话的陈述没错,一个不同的站点肯定不可能知道另外一个站点的 session 数据的。即使是同一个站点的 frontend side 也是没法知道本站点的 session 数据的。但是跟我们要想搞明白的 Rails CSRF 的细节没关系。

其实关键在于,发往服务器的请求中的 token,要么是模版渲染的时候从 session 中读取的,要么是 JS 从本站点的 meta tag 中读取的。那么对于第三方站点上的脚本,这其中的任何一个它都没法拿到,所以这样才防止了 CSRF 攻击。

ecloud 回复

@ecloud 我想你表达的应该是在不同的浏览器窗口打开同一个 web 应用。 你的表述中有一个问题:

当 rails 验证的时候会用最后产生的 authenticity_token 验证

Rails 并不是用最后一个生成的 token,而且你不同的窗口中的打开的应用对应了不同的 session,而 session 存了不同的 token 用于请求的验证。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册