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
|
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
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
20
README.md
20
README.md
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
module RuCaptcha
|
module RuCaptcha
|
||||||
VERSION = '1.0.2'
|
VERSION = '1.1.0'
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue