Rails csrf_token 和 Session CookieStore 原理

Posted by wxianfeng Fri, 01 Mar 2013 22:59:00 GMT

环境: ruby 1.8.7 + rails 2.3.x, ruby 1.9.2 + rails 3.x

前段时间处理手机客户端post请求问题, 经常遇到csrf token 问题, 另外web上也经常遇到和session相关的问题, 不深究下去, 很多东西云里雾里, 于是把rails源码csrf_token, 和 session cookiestore 相关的代码研究了下.

csrf_token 原理

这个相信做rails的, 没有不知道的,是rails framework中为了防止 XSS攻击的, 可是你知道它的原理吗?
好, 顺着代码跟进去, 这是 rails 3.x 的源码.
入口就是 ApplicationController 中的 protect_from_forgery

    # File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 85
       def protect_from_forgery(options = {})
         self.request_forgery_protection_token ||= :authenticity_token
         prepend_before_filter :verify_authenticity_token, options
       end

再找到 verify_authenticity_token

       def verify_authenticity_token
         verified_request? || handle_unverified_request
       end

再找到 verified_request?

发现判断合法的请求方法:

1, 跳过不验证的
2, GET 请求
3, csrf_token 和参数中 authenticity_token 值相同的
4, http header 中 X-CSRF-Token 和 csrf_token 的值相同的

不合法请求会 reset_session

       def handle_unverified_request
         reset_session
       end

来看看 rails 2.x 的, 为什么说 rails 2.x 的,和 rails 3.x 不一样, 影响也很大.

合法的请求:

1, 跳过检查的
2, GET 请求
3, ajax 请求
4, 不是 html 格式请求, 例如 json, xml 格式请求都是合法的
5, csrf_token 值和 参数中 authenticity_token 值相同的

不合法的请求会 raise error

       def verify_authenticity_token
         verified_request? || raise(ActionController::InvalidAuthenticityToken)
       end

上面的处理都有漏洞, 来看看

rails 3.x 的, 假如用户登录是 post, 登录前还没有 session ,此时会 reset_ssession,因为本来就没有登录后的session,reset_session后,后面的代码继续执行, 假如用户知道用户用户名,密码,利用http client 工具就可以成功获得登录后的session, 虽然 csrf 会验证失败, 所以可以自己打个patch使用 rails 2.x 的方式, 直接 raise

rails2.x 的当请求格式不是 html,是 json 就可以成功跳过 csrf 验证, 例如我这个更新redmine的脚本就是利用这个漏洞实现的.

https://gist.github.com/wxianfeng/5070599

那么 csrf_token 的值又是存在什么地方的呢, 在 session[:_csrf_token],rails 默认session是 cookie store, 这就涉及到cookiestore原理了.

关于 csrf_token 还有一个需要注意的地方, 在 test env 下是不需要 csrf_token 的, 顺着 csrf_meta_tag 跟进去可以看到.

Rails Session CookieStore 原理

在rails后端调试下 session, 打印出来的结果是一个hash, 以github 为例, 先反向得到 session 数据, 用firebug可以看到github的cookie中有一个 _gh_session, 如下:

_gh_sess=BAh7CjoPc2Vzc2lvbl9pZCIlMjM1OGMwZjFhYmU2MTQ0MGRlYWUzYWVhODVhM2U2MTk6EF9jc3JmX3Rva2VuSSIxcHNLWEFoYittaXVVVnZXU3BxMDBJaE52Z0QvQ0kyYjg1cU5pNTJMU2R6TT0GOgZFRjoJdXNlcmkD2IsBOhBmaW5nZXJwcmludCIlZGFmNjBhOGFlYTJlZWE3YThjNWY1OGRmMzg2YzhhNWQ6DGNvbnRleHRJIgYvBjsHRg%3D%3D--0320c02623b8a27a66bbbcd38d095511c459e1f3;

取出 — 前面的部分, 假设为 data

::Marshal.load ::Base64.decode64(data) 后会得到一个hash, 这个就是后端的 session数据

ruby-1.9.2-p290 :016 >   data = "BAh7CjoPc2Vzc2lvbl9pZCIlMjM1OGMwZjFhYmU2MTQ0MGRlYWUzYWVhODVhM2U2MTk6EF9jc3JmX3Rva2VuSSIxcHNLWEFoYittaXVVVnZXU3BxMDBJaE52Z0QvQ0kyYjg1cU5pNTJMU2R6TT0GOgZFRjoJdXNlcmkD2IsBOhBmaW5nZXJwcmludCIlZGFmNjBhOGFlYTJlZWE3YThjNWY1OGRmMzg2YzhhNWQ6DGNvbnRleHRJIgYvBjsHRg%3D%3D"
 => "BAh7CjoPc2Vzc2lvbl9pZCIlMjM1OGMwZjFhYmU2MTQ0MGRlYWUzYWVhODVhM2U2MTk6EF9jc3JmX3Rva2VuSSIxcHNLWEFoYittaXVVVnZXU3BxMDBJaE52Z0QvQ0kyYjg1cU5pNTJMU2R6TT0GOgZFRjoJdXNlcmkD2IsBOhBmaW5nZXJwcmludCIlZGFmNjBhOGFlYTJlZWE3YThjNWY1OGRmMzg2YzhhNWQ6DGNvbnRleHRJIgYvBjsHRg%3D%3D" 
ruby-1.9.2-p290 :017 > ::Marshal.load ::Base64.decode64(data)                                                          
=> {:session_id=>"2358c0f1abe61440deae3aea85a3e619", :_csrf_token=>"psKXAhb+miuUVvWSpq00IhNvgD/CI2b85qNi52LSdzM=", :user=>101336, :fingerprint=>"daf60a8aea2eea7a8c5f58df386c8a5d", :context=>"/"}

再来正向生成

data = ::Base64.encode64 Marshal.dump(h)

那—后面的 digest 是怎么生成的, 和rails中 secrect 合起来加密生成的, 这样别人就不能伪造cookie了,术语叫 cookie 签名.

大概生成算法是这样:

session = {"_csrf_token"="xxxxx","user_id"=>4}
session_data = ::Base64.encode64 Marshal.dump(session)
session_data = "#{session_data}--#{generate_hmac(session_data, @secrets.first)}"

def generate_hmac(data, secret)
     OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, data)
end

源码位置:

/Users/wangxianfeng/.rvm/gems/ruby-1.9.2-p290/gems/rack-1.4.3/lib/rack/session/cookie.rb
/home/wxianfeng/.rvm/gems/ruby-1.9.2-p320/gems/activesupport-3.2.2/lib/active_support/message_verifier.rb

总结起来一句话, rails 的session cookiestore 是存在浏览器的cookie中的, 由 http 协议的 headers 带到后端, 后端解包出来的.

OVER, 是不是没想到啊?