Add config.cache_store config.
Store captcha code in Backend cache. Not requirement Session store now.
This commit is contained in:
parent
c9c42de866
commit
d0d6718e15
12
CHANGELOG.md
12
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
|
||||
-----
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
rucaptcha (1.0.2)
|
||||
rucaptcha (1.1.0)
|
||||
railties (>= 3.2)
|
||||
|
||||
GEM
|
||||
|
|
20
README.md
20
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
|
||||
```
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
module RuCaptcha
|
||||
VERSION = '1.0.2'
|
||||
VERSION = '1.1.0'
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue