From d0d6718e1516a1d10028e34565209f271bd3fc1e Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Sat, 29 Oct 2016 10:48:05 +0800 Subject: [PATCH] Add config.cache_store config. Store captcha code in Backend cache. Not requirement Session store now. --- CHANGELOG.md | 12 +++---- Gemfile.lock | 2 +- README.md | 20 +++-------- lib/rucaptcha.rb | 1 + lib/rucaptcha/cache.rb | 20 +++++++---- lib/rucaptcha/configuration.rb | 5 ++- lib/rucaptcha/controller_helpers.rb | 54 ++++++++++++++++++++++------- lib/rucaptcha/engine.rb | 20 +++++++---- lib/rucaptcha/version.rb | 2 +- spec/controller_helpers_spec.rb | 54 +++++++++++++++++++++++------ spec/spec_helper.rb | 4 +++ 11 files changed, 132 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd927a4..e6df400 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,13 @@ -1.0.2 - -- 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 +1.1.0 ----- +- Add `cache_store` config key to setup a cache store location for RuCaptcha. +- Store captcha in custom cache store. + ## Security Notes - Fix Session replay secure issue that when Rails application use CookieStore. - 1.0.0 ----- diff --git a/Gemfile.lock b/Gemfile.lock index eb49863..9fdc381 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - rucaptcha (1.0.2) + rucaptcha (1.1.0) railties (>= 3.2) GEM diff --git a/README.md b/README.md index db349d6..f9d7a63 100644 --- a/README.md +++ b/README.md @@ -45,21 +45,6 @@ brew install imagemagick ghostscript ## 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`: ``` @@ -81,6 +66,11 @@ RuCaptcha.configure do # self.expires_in = 120 # Color style, default: :colorful, allows: [:colorful, :black_white] # 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 ``` diff --git a/lib/rucaptcha.rb b/lib/rucaptcha.rb index d1a8b43..7ca293a 100644 --- a/lib/rucaptcha.rb +++ b/lib/rucaptcha.rb @@ -20,6 +20,7 @@ module RuCaptcha @config.cache_limit = 100 @config.expires_in = 2.minutes @config.style = :colorful + @config.cache_store = Rails.application.config.cache_store @config end diff --git a/lib/rucaptcha/cache.rb b/lib/rucaptcha/cache.rb index 7e7ceaa..8d4a9fd 100644 --- a/lib/rucaptcha/cache.rb +++ b/lib/rucaptcha/cache.rb @@ -1,6 +1,14 @@ require 'fileutils' 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 module Cache def self.prepended(base) @@ -11,7 +19,7 @@ module RuCaptcha module ClassMethods def create(code) - cache.fetch(code, expires_in: 1.days) do + file_cache.fetch(code, expires_in: 1.days) do super(code) end end @@ -26,15 +34,15 @@ module RuCaptcha code end - def cache - return @cache if defined?(@cache) + def file_cache + return @file_cache if defined?(@file_cache) cache_path = Rails.root.join('tmp', 'cache', 'rucaptcha') 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 - @cache.cleanup - @cache + @file_cache.cleanup + @file_cache end def cached_codes diff --git a/lib/rucaptcha/configuration.rb b/lib/rucaptcha/configuration.rb index 7a87cf7..4330e33 100644 --- a/lib/rucaptcha/configuration.rb +++ b/lib/rucaptcha/configuration.rb @@ -6,12 +6,15 @@ module RuCaptcha attr_accessor :len # implode, default 0.3 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 # set 0 to disable limit and file cache, default: 100 attr_accessor :cache_limit # Color style, default: :colorful, allows: [:colorful, :black_white] attr_accessor :style - # session[:_rucaptcha] expire time, default 2 minutes + # rucaptcha expire time, default 2 minutes attr_accessor :expires_in end end diff --git a/lib/rucaptcha/controller_helpers.rb b/lib/rucaptcha/controller_helpers.rb index ceb9c76..771ed95 100644 --- a/lib/rucaptcha/controller_helpers.rb +++ b/lib/rucaptcha/controller_helpers.rb @@ -6,28 +6,56 @@ module RuCaptcha helper_method :verify_rucaptcha? end - def generate_rucaptcha - session[:_rucaptcha] = RuCaptcha::Captcha.random_chars - session[:_rucaptcha_at] = Time.now.to_i + def rucaptcha_sesion_key_key + ['rucaptcha-session', session.id].join(':') + 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 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 chars in Session expire in 2 minutes - valid = false - if (Time.now.to_i - rucaptcha_at) <= RuCaptcha.config.expires_in - valid = captcha.present? && captcha == session.delete(:_rucaptcha) + if captcha.blank? + return add_rucaptcha_validation_error end - if resource && resource.respond_to?(:errors) - resource.errors.add(:base, t('rucaptcha.invalid')) unless valid + if captcha != store_info[:code] + return add_rucaptcha_validation_error 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 diff --git a/lib/rucaptcha/engine.rb b/lib/rucaptcha/engine.rb index 2fc4a14..f3a994e 100644 --- a/lib/rucaptcha/engine.rb +++ b/lib/rucaptcha/engine.rb @@ -8,13 +8,19 @@ module RuCaptcha RuCaptcha::Captcha.send(:prepend, RuCaptcha::Cache) end - if Rails.application.config.session_store.name.match(/CookieStore/) - puts %( -[RuCaptcha] Your application session has use #{Rails.application.config.session_store} -this may have Session [Replay Attacks] secure issue in RuCaptcha case. -We suggest you change it to backend [:active_record_store, :redis_session_store] -http://guides.rubyonrails.org/security.html#replay-attacks-for-cookiestore-sessions) - puts "" + cache_store = RuCaptcha.config.cache_store + store_name = cache_store.is_a?(Array) ? cache_store.first : cache_store + if [:memory_store, :null_store, :file_store].include?(store_name) + raise " + + RuCaptcha's cache_store requirements are stored across processes and machines, + 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 diff --git a/lib/rucaptcha/version.rb b/lib/rucaptcha/version.rb index 31683d2..25c3e7e 100644 --- a/lib/rucaptcha/version.rb +++ b/lib/rucaptcha/version.rb @@ -1,3 +1,3 @@ module RuCaptcha - VERSION = '1.0.2' + VERSION = '1.1.0' end diff --git a/spec/controller_helpers_spec.rb b/spec/controller_helpers_spec.rb index f1db008..bddf38c 100644 --- a/spec/controller_helpers_spec.rb +++ b/spec/controller_helpers_spec.rb @@ -1,23 +1,45 @@ require 'spec_helper' +require 'securerandom' describe RuCaptcha do + class CustomSession + attr_accessor :id + + def initialize + self.id = SecureRandom.hex + end + end class Simple < ActionController::Base def session - @session ||= {} + @session ||= CustomSession.new end def params @params ||= {} 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 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 it 'should work' do expect(RuCaptcha::Captcha).to receive(:random_chars).and_return('abcd') 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 @@ -29,7 +51,7 @@ describe RuCaptcha do end it 'should work when session[:_rucaptcha] is nil' do - simple.session[:_rucaptcha] = nil + simple.clean_custom_session simple.params[:_rucaptcha] = 'Abcd' expect(simple.verify_rucaptcha?).to eq(false) end @@ -37,11 +59,18 @@ describe RuCaptcha do context 'Correct chars in params' do it 'should work' do - simple.session[:_rucaptcha_at] = Time.now.to_i - simple.session[:_rucaptcha] = 'abcd' + Rails.cache.write(simple.rucaptcha_sesion_key_key, { + time: Time.now.to_i, + code: 'abcd' + }) simple.params[:_rucaptcha] = 'Abcd' 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' expect(simple.verify_rucaptcha?).to eq(true) end @@ -49,17 +78,22 @@ describe RuCaptcha do describe 'Incorrect chars' do it 'should work' do - simple.session[:_rucaptcha_at] = Time.now.to_i - 60 - simple.session[:_rucaptcha] = 'abcd' + Rails.cache.write(simple.rucaptcha_sesion_key_key, { + time: Time.now.to_i - 60, + code: 'abcd' + }) simple.params[:_rucaptcha] = 'd123' expect(simple.verify_rucaptcha?).to eq(false) + expect(simple.custom_session).to eq nil end end describe 'Expires Session key' do it 'should work' do - simple.session[:_rucaptcha_at] = Time.now.to_i - 121 - simple.session[:_rucaptcha] = 'abcd' + Rails.cache.write(simple.rucaptcha_sesion_key_key, { + time: Time.now.to_i - 121, + code: 'abcd' + }) simple.params[:_rucaptcha] = 'abcd' expect(simple.verify_rucaptcha?).to eq(false) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 75c1ef6..7549506 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -13,6 +13,10 @@ module Rails def root Pathname.new(File.join(File.dirname(__FILE__), '..')) end + + def cache + @cache ||= ActiveSupport::Cache::MemoryStore.new + end end end