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
- 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
-----

View File

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

View File

@ -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
```

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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