Rails Rails 中消失的 CSRF token

warmwind · October 01, 2014 · Last by hiveer replied at October 24, 2018 · 14201 hits

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 攻击。

Reply to ecloud

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

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

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

You need to Sign in before reply, if you don't have an account, please Sign up first.