Add config.cache_store config.

Store captcha code in Backend cache.
Not requirement Session store now.
This commit is contained in:
Jason Lee 2016-10-29 10:48:05 +08:00
parent c9c42de866
commit d0d6718e15
11 changed files with 132 additions and 62 deletions

View File

@ -1,17 +1,13 @@
1.0.2 1.1.0
- Revert 1.0.1 changes, still store code in Session, `Rails.cache` not a not place in difference environments.
for exampe: Not enable cache, File cache will have bug.
- Give a warning when user use CookieStore.
1.0.1
----- -----
- Add `cache_store` config key to setup a cache store location for RuCaptcha.
- Store captcha in custom cache store.
## Security Notes ## Security Notes
- Fix Session replay secure issue that when Rails application use CookieStore. - Fix Session replay secure issue that when Rails application use CookieStore.
1.0.0 1.0.0
----- -----

View File

@ -1,7 +1,7 @@
PATH PATH
remote: . remote: .
specs: specs:
rucaptcha (1.0.2) rucaptcha (1.1.0)
railties (>= 3.2) railties (>= 3.2)
GEM GEM

View File

@ -45,21 +45,6 @@ brew install imagemagick ghostscript
## Usage ## Usage
**Security Notice!**
You need change your application Session store from `CookieStore` (Rails default) to backend store location
for avoid [Session Replay Attacks](http://guides.rubyonrails.org/security.html#replay-attacks-for-cookiestore-sessions) security issue.
- [:active_session_store](https://github.com/rails/activerecord-session_store)
- [:memcached_store](http://api.rubyonrails.org/classes/ActionDispatch/Session/MemCacheStore.html)
- [:redis_session_store](https://github.com/roidrage/redis-session-store)
config/initializers/session_store.rb
```rb
Rails.application.config.session_store :redis_session_store, { ... }
```
Put rucaptcha in your `Gemfile`: Put rucaptcha in your `Gemfile`:
``` ```
@ -81,6 +66,11 @@ RuCaptcha.configure do
# self.expires_in = 120 # self.expires_in = 120
# Color style, default: :colorful, allows: [:colorful, :black_white] # Color style, default: :colorful, allows: [:colorful, :black_white]
# self.style = :colorful # self.style = :colorful
# [Requirement]
# Store Captcha code where, this config more like Rails config.cache_store
# default: Rails application config.cache_store
# But RuCaptcha requirements cache_store not in [:null_store, :memory_store, :file_store]
self.cache_store = :mem_cache_store
end end
``` ```

View File

@ -20,6 +20,7 @@ module RuCaptcha
@config.cache_limit = 100 @config.cache_limit = 100
@config.expires_in = 2.minutes @config.expires_in = 2.minutes
@config.style = :colorful @config.style = :colorful
@config.cache_store = Rails.application.config.cache_store
@config @config
end end

View File

@ -1,6 +1,14 @@
require 'fileutils' require 'fileutils'
module RuCaptcha module RuCaptcha
class << self
def cache
return @cache if defined? @cache
@cache = ActiveSupport::Cache.lookup_store(RuCaptcha.config.cache_store)
@cache
end
end
# File Cache # File Cache
module Cache module Cache
def self.prepended(base) def self.prepended(base)
@ -11,7 +19,7 @@ module RuCaptcha
module ClassMethods module ClassMethods
def create(code) def create(code)
cache.fetch(code, expires_in: 1.days) do file_cache.fetch(code, expires_in: 1.days) do
super(code) super(code)
end end
end end
@ -26,15 +34,15 @@ module RuCaptcha
code code
end end
def cache def file_cache
return @cache if defined?(@cache) return @file_cache if defined?(@file_cache)
cache_path = Rails.root.join('tmp', 'cache', 'rucaptcha') cache_path = Rails.root.join('tmp', 'cache', 'rucaptcha')
FileUtils.mkdir_p(cache_path) unless File.exist? cache_path FileUtils.mkdir_p(cache_path) unless File.exist? cache_path
@cache = ActiveSupport::Cache::FileStore.new(cache_path) @file_cache = ActiveSupport::Cache::FileStore.new(cache_path)
# clear expired captcha cache files on Process restart # clear expired captcha cache files on Process restart
@cache.cleanup @file_cache.cleanup
@cache @file_cache
end end
def cached_codes def cached_codes

View File

@ -6,12 +6,15 @@ module RuCaptcha
attr_accessor :len attr_accessor :len
# implode, default 0.3 # implode, default 0.3
attr_accessor :implode attr_accessor :implode
# Store Captcha code where, this config more like Rails config.cache_store
# default: Rails application config.cache_store
attr_accessor :cache_store
# Number of Captcha codes limit # Number of Captcha codes limit
# set 0 to disable limit and file cache, default: 100 # set 0 to disable limit and file cache, default: 100
attr_accessor :cache_limit attr_accessor :cache_limit
# Color style, default: :colorful, allows: [:colorful, :black_white] # Color style, default: :colorful, allows: [:colorful, :black_white]
attr_accessor :style attr_accessor :style
# session[:_rucaptcha] expire time, default 2 minutes # rucaptcha expire time, default 2 minutes
attr_accessor :expires_in attr_accessor :expires_in
end end
end end

View File

@ -6,28 +6,56 @@ module RuCaptcha
helper_method :verify_rucaptcha? helper_method :verify_rucaptcha?
end end
def generate_rucaptcha def rucaptcha_sesion_key_key
session[:_rucaptcha] = RuCaptcha::Captcha.random_chars ['rucaptcha-session', session.id].join(':')
session[:_rucaptcha_at] = Time.now.to_i end
RuCaptcha::Captcha.create(session[:_rucaptcha]) def generate_rucaptcha
code = RuCaptcha::Captcha.random_chars
session_val = {
code: code,
time: Time.now.to_i
}
RuCaptcha.cache.write(rucaptcha_sesion_key_key, session_val, expires_in: RuCaptcha.config.expires_in)
RuCaptcha::Captcha.create(code)
end end
def verify_rucaptcha?(resource = nil) def verify_rucaptcha?(resource = nil)
rucaptcha_at = session[:_rucaptcha_at].to_i store_info = RuCaptcha.cache.read(rucaptcha_sesion_key_key)
# make sure move used key
RuCaptcha.cache.delete(rucaptcha_sesion_key_key)
# Make sure session exist
if store_info.blank?
return add_rucaptcha_validation_error
end
# Make sure not expire
puts "-------------- #{store_info.inspect}"
if (Time.now.to_i - store_info[:time]) > RuCaptcha.config.expires_in
return add_rucaptcha_validation_error
end
# Make sure parama have captcha
captcha = (params[:_rucaptcha] || '').downcase.strip captcha = (params[:_rucaptcha] || '').downcase.strip
if captcha.blank?
# Captcha chars in Session expire in 2 minutes return add_rucaptcha_validation_error
valid = false
if (Time.now.to_i - rucaptcha_at) <= RuCaptcha.config.expires_in
valid = captcha.present? && captcha == session.delete(:_rucaptcha)
end end
if resource && resource.respond_to?(:errors) if captcha != store_info[:code]
resource.errors.add(:base, t('rucaptcha.invalid')) unless valid return add_rucaptcha_validation_error
end end
valid true
end
private
def add_rucaptcha_validation_error
if defined?(resource) && resource && resource.respond_to?(:errors)
resource.errors.add(:base, t('rucaptcha.invalid'))
end
false
end end
end end
end end

View File

@ -8,13 +8,19 @@ module RuCaptcha
RuCaptcha::Captcha.send(:prepend, RuCaptcha::Cache) RuCaptcha::Captcha.send(:prepend, RuCaptcha::Cache)
end end
if Rails.application.config.session_store.name.match(/CookieStore/) cache_store = RuCaptcha.config.cache_store
puts %( store_name = cache_store.is_a?(Array) ? cache_store.first : cache_store
[RuCaptcha] Your application session has use #{Rails.application.config.session_store} if [:memory_store, :null_store, :file_store].include?(store_name)
this may have Session [Replay Attacks] secure issue in RuCaptcha case. raise "
We suggest you change it to backend [:active_record_store, :redis_session_store]
http://guides.rubyonrails.org/security.html#replay-attacks-for-cookiestore-sessions) RuCaptcha's cache_store requirements are stored across processes and machines,
puts "" such as :mem_cache_store, :redis_store, or other distributed storage.
But your current set is :#{store_name}.
Please make config file `config/initializes/rucaptcha.rb` to setup `cache_store`.
More infomation please read GitHub rucaptcha README file.
"
end end
end end
end end

View File

@ -1,3 +1,3 @@
module RuCaptcha module RuCaptcha
VERSION = '1.0.2' VERSION = '1.1.0'
end end

View File

@ -1,23 +1,45 @@
require 'spec_helper' require 'spec_helper'
require 'securerandom'
describe RuCaptcha do describe RuCaptcha do
class CustomSession
attr_accessor :id
def initialize
self.id = SecureRandom.hex
end
end
class Simple < ActionController::Base class Simple < ActionController::Base
def session def session
@session ||= {} @session ||= CustomSession.new
end end
def params def params
@params ||= {} @params ||= {}
end end
def custom_session
Rails.cache.read(self.rucaptcha_sesion_key_key)
end
def clean_custom_session
Rails.cache.delete(self.rucaptcha_sesion_key_key)
end
end end
let(:simple) { Simple.new } let(:simple) { Simple.new }
describe '.rucaptcha_sesion_key_key' do
it 'should work' do
expect(simple.rucaptcha_sesion_key_key).to eq ['rucaptcha-session', simple.session.id].join(':')
end
end
describe '.generate_rucaptcha' do describe '.generate_rucaptcha' do
it 'should work' do it 'should work' do
expect(RuCaptcha::Captcha).to receive(:random_chars).and_return('abcd') expect(RuCaptcha::Captcha).to receive(:random_chars).and_return('abcd')
expect(simple.generate_rucaptcha).not_to be_nil expect(simple.generate_rucaptcha).not_to be_nil
expect(simple.session[:_rucaptcha]).to eq('abcd') expect(simple.custom_session[:code]).to eq('abcd')
end end
end end
@ -29,7 +51,7 @@ describe RuCaptcha do
end end
it 'should work when session[:_rucaptcha] is nil' do it 'should work when session[:_rucaptcha] is nil' do
simple.session[:_rucaptcha] = nil simple.clean_custom_session
simple.params[:_rucaptcha] = 'Abcd' simple.params[:_rucaptcha] = 'Abcd'
expect(simple.verify_rucaptcha?).to eq(false) expect(simple.verify_rucaptcha?).to eq(false)
end end
@ -37,11 +59,18 @@ describe RuCaptcha do
context 'Correct chars in params' do context 'Correct chars in params' do
it 'should work' do it 'should work' do
simple.session[:_rucaptcha_at] = Time.now.to_i Rails.cache.write(simple.rucaptcha_sesion_key_key, {
simple.session[:_rucaptcha] = 'abcd' time: Time.now.to_i,
code: 'abcd'
})
simple.params[:_rucaptcha] = 'Abcd' simple.params[:_rucaptcha] = 'Abcd'
expect(simple.verify_rucaptcha?).to eq(true) expect(simple.verify_rucaptcha?).to eq(true)
simple.session[:_rucaptcha] = 'abcd' expect(simple.custom_session).to eq nil
Rails.cache.write(simple.rucaptcha_sesion_key_key, {
time: Time.now.to_i,
code: 'abcd'
})
simple.params[:_rucaptcha] = 'AbcD' simple.params[:_rucaptcha] = 'AbcD'
expect(simple.verify_rucaptcha?).to eq(true) expect(simple.verify_rucaptcha?).to eq(true)
end end
@ -49,17 +78,22 @@ describe RuCaptcha do
describe 'Incorrect chars' do describe 'Incorrect chars' do
it 'should work' do it 'should work' do
simple.session[:_rucaptcha_at] = Time.now.to_i - 60 Rails.cache.write(simple.rucaptcha_sesion_key_key, {
simple.session[:_rucaptcha] = 'abcd' time: Time.now.to_i - 60,
code: 'abcd'
})
simple.params[:_rucaptcha] = 'd123' simple.params[:_rucaptcha] = 'd123'
expect(simple.verify_rucaptcha?).to eq(false) expect(simple.verify_rucaptcha?).to eq(false)
expect(simple.custom_session).to eq nil
end end
end end
describe 'Expires Session key' do describe 'Expires Session key' do
it 'should work' do it 'should work' do
simple.session[:_rucaptcha_at] = Time.now.to_i - 121 Rails.cache.write(simple.rucaptcha_sesion_key_key, {
simple.session[:_rucaptcha] = 'abcd' time: Time.now.to_i - 121,
code: 'abcd'
})
simple.params[:_rucaptcha] = 'abcd' simple.params[:_rucaptcha] = 'abcd'
expect(simple.verify_rucaptcha?).to eq(false) expect(simple.verify_rucaptcha?).to eq(false)
end end

View File

@ -13,6 +13,10 @@ module Rails
def root def root
Pathname.new(File.join(File.dirname(__FILE__), '..')) Pathname.new(File.join(File.dirname(__FILE__), '..'))
end end
def cache
@cache ||= ActiveSupport::Cache::MemoryStore.new
end
end end
end end