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, 是不是没想到啊?


ruby 对象模型

Posted by wxianfeng Mon, 07 Jan 2013 21:54:00 GMT

环境: mac 10.8 + ruby 1.9.2
ruby 对象模型非常重要, 不了解ruby对象模型, 不算真正掌握ruby, ruby 号称一切都是对象, why? 当你知道对象模型就清楚了.

说到ruby对象模型, 这张图是ruby基本的类关系.

class 表示实例关系(水平),super 表示继承关系(垂直),从图中可以看出

objmy 是 MyClass 的实例
MyClass 是 Class 的实例
Class 的父类是 Module
MyClass 的父类是 Object
Object 的父类是 BasicObject (ruby1.9)
MyClass include 模块M后,相当于在继承祖先链正上方继承该模块
Object include 模块Kernel
Module类的superclass 是Object
所有都可以在irb看到:

ruby-1.9.2-p290 :001 > MyClass = Class.new
 => MyClass 
ruby-1.9.2-p290 :002 > objmy = MyClass.new
 => #<MyClass:0x007f9ea1aba080> 
ruby-1.9.2-p290 :003 > objmy.class
 => MyClass 
ruby-1.9.2-p290 :004 > MyClass.class
 => Class 
ruby-1.9.2-p290 :005 > MyClass.superclass
 => Object 
ruby-1.9.2-p290 :006 > Class.class
 => Class 
ruby-1.9.2-p290 :007 > Class.superclass
 => Module 
ruby-1.9.2-p290 :008 > Module.class
 => Class 
ruby-1.9.2-p290 :009 > Module.superclass
 => Object 
ruby-1.9.2-p290 :010 > Object.class
 => Class 
ruby-1.9.2-p290 :011 > Object.superclass
 => BasicObject 
ruby-1.9.2-p290 :012 > BasicObject.class
 => Class 
ruby-1.9.2-p290 :013 > BasicObject.superclass
 => nil 

所以掌握了 Object,Module,Class 三者之间关系很重要, ruby 很多gem里的方法都是打开 Object来注入方法的.这样下面任何对象就都有该实例方法了.

demo:

#!/usr/bin/env ruby

class Object
        def foo
                p "im foo"
        end
end

MyClass = Class.new
objmy = MyClass.new
objmy.foo # im foo

有一个gem可以查看ruby对象模型, drx
安装:

>gem install drx
>brew install graphviz

使用:

wxianfeng-2:entos wangxianfeng$ irb
ruby-1.9.2-p290 :001 > require 'drx'
 => true 
ruby-1.9.2-p290 :002 > MyClass = Class.new
 => MyClass 
ruby-1.9.2-p290 :003 > objmy = MyClass.new
 => #<MyClass:0x007fcd1221b2e0> 
ruby-1.9.2-p290 :004 > objmy.see

出现下图:

这样就更加一目了然了.但是注意上图引入了eigenclass概念, 想了解 ruby 中方法查找的话, 就必须要知道了, 下回分解.


ruby 汉字转拼音

Posted by wxianfeng Tue, 20 Nov 2012 04:30:00 GMT

很久以前从项目中抽取出来的 汉字转拼音的 gem, 今天介绍下使用方法.

功能

  • 首字母支持
  • 全拼支持
  • 多音字支持
  • 其它字符默认输出

Gemfile:

gem 'hanzi_to_pinyin', '0.8.0', require: 'hanzi_to_pinyin'

使用demo

$ HanziToPinyin.hanzi_to_pinyin("喜欢Ruby") => "xhruby"
$ HanziToPinyin.hanzi_2_pinyin("喜欢Ruby") => "xhruby"

$ HanziToPinyin.is_hanzi?("") => true
$ HanziToPinyin.is_hanzi?("a") => false

# 多音字,分隔 字字之间;分隔,字母丢弃
$ HanziToPinyin.hanzi_2_py("我们") => "wo;men"
$ HanziToPinyin.hanzi_2_py("查理Smith") => "cha,zha;li"
$ HanziToPinyin.hanzi_2_py("测试1") => "ce;shi;1"
$ HanziToPinyin.hanzi_2_py("测_试") => "ce;_;shi"
$ HanziToPinyin.hanzi_2_py("测-试") => "ce;-;shi"
$ HanziToPinyin.hanzi_2_py(2) =>  "2"

$ HanziToPinyin.is_number?("1".ord) => true
$ HanziToPinyin.is_number?("a".ord) => false

$ HanziToPinyin.is_underline?("_".ord) => true
$ HanziToPinyin.is_underline?("豆豆") => false
$ HanziToPinyin.is_dash?("-".ord) => true

更多介绍可以看 github 主页

HERE

另外也被翻译成了 nodejs module

nodejs


WEB GUI 执行rake任务

Posted by wxianfeng Sat, 15 Dec 2012 02:51:00 GMT

情况是这样的, 公司内部测试服务器经常需要更新代码供测试人员使用网站, 每次都是我们后端开发人员部署的, 这样就加大了工作量,效率低下,话说我们部署也是使用capistrano 的, 只需一条命令就可以顺利部署, 但是还是不如非开发人员部署来的方便,于是就有了 rake_ui
rake_ui gem 是我发布的,但是是在修改别人代码的基础上发布的,下面介绍使用方法:

首先看下效果图:

1, 环境

Node.js
Socket.io
Rails 3.x

2,Gemfile

gem 'rake_ui', '0.6.0'

3, 在你的 routes.rb 中添加路由

Rails.application.routes.draw do
  mount RakeUi::Engine => "/rake_ui"
end

4, 配置 config/rake_ui.yml

host: '192.168.10.107'
log: '/data/projects/entos/log/rake.log'

host是你的ip地址,Nodejs 要用, log 是你项目下log目录下rake.log 会被自动创建

5, 配置 config/tasks.yml

- 'rake about'  
- 'rake routes'

把你需要执行的rake任务写在这个 yaml 中

6, 启动 nodejs server

rake start_node_server

ok, 你可以访问 /rake_ui 看到你的 web gui 界面了, 把你的部署方案写在rake任务中, 然后在这个界面可以点击部署.

该gem有可能被更新,看到最新的说明请移到步这里:

HERE


ruby Array#pack String#unpack 实例

Posted by wxianfeng Sat, 26 May 2012 21:25:00 GMT

最近项目中常用到 Array#pack, String#unpack 方法,在此总结下:

Array#pack, String#unpack 可以实现不同编码之间的处理, 可以处理字节级, bit 级的一些二进制格式.

字节编码, ruby里主要是 “\nnn” 和 “\xnn” 的形式, nnn 是八进制数字, nn 是十六进制, 可以从 <<ruby编程语言>> 这本书看到相关信息.

截了张书中的图:

例子:
ASCII 码值: 0123456789
字节编码:
“\000\001\002\003\004\005\006\007\010\011” (八进制)
“\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09” (十六进制)

另外 ascii 码值是 7,8,9 的在ruby中是转义序列 “\a”, “\b”, “\t”, 所以字节编码也可以写成
\x00\x01\x02\x03\x04\x05\x06\a\b\t

1.9.2p290 :198 > a = [0,1,2,3,4,5,6,7,8,9]
 => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1.9.2p290 :199 > a.pack("c*")
 => "\x00\x01\x02\x03\x04\x05\x06\a\b\t"
1.9.2p290 :200 > "\x00\x01\x02\x03\x04\x05\x06\a\b\t" == "\000\001\002\003\004\005\006\007\010\011"
 => true
1.9.2p290 :201 > "\011" == "\x09"
 => true
1.9.2p290 :202 > "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09" == "\000\001\002\003\004\005\006\007\010\011"
 => true

下面来看一些实例,以字符模板来讲

1, M

M         | String  | quoted printable, MIME encoding (see RFC2045)

字符串和 quoted printable 编码之间转换,常用在邮件编码中

Array#pack
ruby-1.9.2-p290 :132 > ["[www.wxianfeng.com]欢迎您注册,请您激活"].pack("M")
 => "[www.wxianfeng.com]=E6=AC=A2=E8=BF=8E=E6=82=A8=E6=B3=A8=E5=86=8C,=E8=AF=B7=\n=E6=82=A8=E6=BF=80=E6=B4=BB=\n" 

可以看到 每76个字符就多了一个 =\n , 所以如果是用在邮件的 Subject 的中的话, 应该是

["str"].pack("M").gsub(/=\n/,"")

完整的邮件Subject编码应该是 像这样:

value = ["[#{site}]请激活您的帐号"].pack("M").gsub(/=\n/, "")
subject = "=?UTF-8?Q?#{value}?="
String#unpack
ruby-1.9.2-p290 :153 > "[www.wxianfeng.com]=E6=AC=A2=E8=BF=8E=E6=82=A8=E6=B3=A8=E5=86=8C,=E8=AF=B7=\n=E6=82=A8=E6=BF=80=E6=B4=BB=\n".unpack("M")
 => ["[www.wxianfeng.com]\xE6\xAC\xA2\xE8\xBF\x8E\xE6\x82\xA8\xE6\xB3\xA8\xE5\x86\x8C,\xE8\xAF\xB7\xE6\x82\xA8\xE6\xBF\x80\xE6\xB4\xBB"] 

2, m

m         | String  | base64 encoded string (see RFC 2045, count is width)
             |         | (if count is 0, no line feed are added, see RFC 4648)

字符串和 Base64 编码之间转换

Array#pack
ruby-1.9.2-p290 :133 > ["[www.wxianfeng.com]欢迎您注册,请您激活"].pack("m")
 => "W3d3dy53eGlhbmZlbmcuY29tXeasoui/juaCqOazqOWGjCzor7fmgqjmv4Dm\ntLs=\n"

base64编码也可以用在邮件编码中,例如用在Subject中就是这样:

value = ["[#{site}]请激活您的帐号"].pack("M").gsub(/=\n/, "")
subject = "=?UTF-8?B?#{value}?="
String#unpack
ruby-1.9.2-p290 :155 > "W3d3dy53eGlhbmZlbmcuY29tXeasoui/juaCqOazqOWGjCzor7fmgqjmv4Dm\ntLs=\n".unpack("m")
 => ["[www.wxianfeng.com]\xE6\xAC\xA2\xE8\xBF\x8E\xE6\x82\xA8\xE6\xB3\xA8\xE5\x86\x8C,\xE8\xAF\xB7\xE6\x82\xA8\xE6\xBF\x80\xE6\xB4\xBB"]

3, L

 L         | Integer | 32-bit unsigned, native endian (uint32_t)

整型(ASCII)和二进制字符串相互转化,int是32为无符号的,占4个字节

Array#pack
ruby-1.9.2-p290 :139 > [65].pack("L")
 => "A\x00\x00\x00"

String#unpack
ruby-1.9.2-p290 :140 > "A\x00\x00\x00".unpack("L")
 => [65] 

4, c

c         | Integer | 8-bit signed (signed char)

整型(ASCII)和二进制字符串相互转化,int 是8位有符号的,占一个字节

Array#pack
ruby-1.9.2-p290 :142 > [77].pack("c")
 => "M" 

String#unpack
ruby-1.9.2-p290 :143 > "M".unpack("c")
 => [77] 

5, Q

Q         | Integer | 64-bit unsigned, native endian (uint64_t)

整型和二进制字符串相互转化,int是64位无符号的,占8字节

Array#pack
ruby-1.9.2-p290 :149 > [1338053358065].pack("Q")
 => "\xF1\xF11\x8A7\x01\x00\x00" 

String#unpack
ruby-1.9.2-p290 :150 > "\xF1\xF11\x8A7\x01\x00\x00".unpack("Q")
 => [1338053358065]

6, S

S         | Integer | 16-bit unsigned, native endian (uint16_t)

整型和二进制字符串转化,int是16位无符号的,占2个字节

Array#pack
ruby-1.9.2-p290 :151 > [6].pack("S")
 => "\x06\x00" 

String#unpack
ruby-1.9.2-p290 :152 > "\x06\x00".unpack("S")
 => [6] 

http://www.ruby-doc.org/core-1.9.3/Array.html#method-i-pack
http://www.ruby-doc.org/core-1.9.3/String.html#method-i-unpack
http://www.cnblogs.com/baochun968/archive/2011/10/19/2218008.html


capistrano 多机部署

Posted by wxianfeng Thu, 12 Apr 2012 03:39:00 GMT

核心使用 task 指令 实现多机部署

# encoding:utf-8
# >cap local deploy
# >cap remote deploy

set :application, "entos"
set :deploy_to, "/data/projects/entos"

set :scm, "git"
set :repository,  "git@114.255.155.167:entos.git"
set :branch, "master"
set :use_sudo, false
set :rails_env,"production"

task :remote do
  set :user, "entsea"
  set :deploy_via, :remote_cache
  set :copy_exclude, %w(external)
  server "114.255.155.166", :web, :app, :db, :primary => true
end

task :local do
  set :user, 'zzq'
  set :deploy_via, :remote_cache
  set :copy_exclude, %w(external)
  server '192.168.10.105', :web, :app, :db, :primary => true
end

namespace :deploy do
  task :start do; end
  task :stop do; end

  desc "Creating ln -s , example: database.yml"
  task :create_sync do
    run "ln -s #{shared_path}/config/database.yml #{current_path}/config/database.yml"
  end

  desc "Restarting unicorn"
  task :restart, :roles => :app, :except => { :no_release => true } do
    # run "/bin/sh restart_server.sh"
  end
end

after "deploy:symlink", "deploy:create_sync"

正则 ?: 非捕获组

Posted by wxianfeng Wed, 21 Mar 2012 03:03:00 GMT

今天一个同事 问我 正则 里 ?: 什么意思,记得 以前知道的 , 愣是 忘记了 ,查了下 是非捕获组的意思!

捕获组
()内的是分组,可以用 $1,$2…取值, 被存在了内存中, 留反向引用.
demo:

ruby-1.9.2-p290 :022 >   "abcabc".match(/(abc)/)
 => #<MatchData "abc" 1:"abc"> 
ruby-1.9.2-p290 :023 > $1
 => "abc" 

非捕获组
(?:) 内的不当作分组, 分组内内容,内存中没有.
demo:

ruby-1.9.2-p290 :024 > "abcabc".match(/(?:abc)/)
 => #<MatchData "abc"> 
ruby-1.9.2-p290 :025 > $1
 => nil 

那么这个有什么用呢, 可以节省内存

demo

ruby-1.9.2-p290 :033 > "hello" =~ /h(i|ello)/
 => 0 
ruby-1.9.2-p290 :033 > $1
 => "ello" 
ruby-1.9.2-p290 :034 > "hello" =~ /h(?:i|ello)/
 => 0 
ruby-1.9.2-p290 :035 > $1
=> nil

北京 798 Ruby/Rails 活动

Posted by wxianfeng Fri, 29 Jul 2011 18:36:00 GMT

时间: 2011-07-24
收获:
发现北京ROR的公司不是一般的多,签到单上看到N多公司,技术上没有太大收获,都是介绍性的,没有实战性的,内容主要涉及: mirah , Mongodb,Erlang,Grape

进程:http://www.surveymonkey.com/s/MSY2L7T

PS : 798 很好玩,很有艺术特色

现场:

093

798 入口

059

Ruby活动地方

054

Rails rumble 创始人

049

现场job board

067

现场

061

清一色老外,清一色Mac

056

介绍Mirah

045

798

083

798

043

798

MORE


shell 文件 插入行

Posted by wxianfeng Sun, 10 Jul 2011 22:44:00 GMT

一个rails2.x 的项目,需要迁移到rails3.x , ruby 1.9.2的编码问题,需要在rb文件头添加指定编码, 常见指定方式如下:

#coding:utf-8
#encoding:utf-8
# -*- coding: utf-8 -*-
# -*- encoding: utf-8 -*-

那么多rb文件总不能一个一个加吧,写个shell解决之!!!


ruby yaml 表示数组

Posted by wxianfeng Tue, 01 Mar 2011 00:24:00 GMT

yaml 的语法真是变态 , 表示个数组这么麻烦, 更复杂的数据结构 那不是更麻烦 !!!

yaml 文件:

# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html

one:
  name: MyString
  orgunit_id: 1
  inheritable: false
 # codes 是yaml数组表示方法
 # 缩进只能是两个空格为一级,不能是其他字符
  codes: 
    - 1
    - a
    - 2
    - b
    - 3
    - c

ruby 解析yaml:

ruby-1.9.2-p0 >   file =  "#{Rails.root}/test/fixtures/enumerations.yml"
 => "/usr/local/system/projects/entos/ent_os/test/fixtures/enumerations.yml" 
ruby-1.9.2-p0 > YAML.load File.read(file) 
 => {"one"=>{"name"=>"MyString", "orgunit_id"=>1, "inheritable"=>false, "codes"=>[1, "a", 2, "b", 3, "c"]}} 

不知道怎么写的可以 使用 to_yaml 方法 看一下:

irb(main):001:0> 
=> {"one"=>{"name"=>"MyString", "inheritable"=>false, "orgunit_id"=>1, "codes"=>[1, "a", 2, "b", 3, "c"]}}
irb(main):002:0> require "yaml"
=> true
irb(main):003:0> hsh.to_yaml
=> "--- \none: \n  name: MyString\n  inheritable: false\n  orgunit_id: 1\n  codes: \n  - 1\n  - a\n  - 2\n  - b\n  - 3\n  - c\n"

可读性 更好的 使用 y 方法

ruby-1.9.2-p0 > y hsh
--- 
one: 
  name: MyString
  orgunit_id: 1
  inheritable: false
  codes: 
  - 1
  - a
  - 2
  - b
  - 3
  - c
 => nil