bot-away 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +7 -0
- data/History.txt +12 -2
- data/LICENSE +20 -0
- data/README.rdoc +124 -67
- data/Rakefile +57 -0
- data/VERSION +1 -0
- data/bot-away.gemspec +96 -0
- data/lib/bot-away.rb +67 -7
- data/lib/bot-away/action_dispatch/request.rb +20 -0
- data/lib/bot-away/action_view/helpers/instance_tag.rb +13 -4
- data/lib/bot-away/param_parser.rb +25 -12
- data/lib/bot-away/spinner.rb +0 -2
- data/spec/controllers/test_controller_spec.rb +82 -35
- data/spec/rspec_version.rb +19 -0
- data/spec/spec_helper.rb +103 -2
- data/spec/support/obfuscation_helper.rb +102 -47
- data/spec/support/rails/mock_logger.rb +21 -0
- data/spec/support/test_controller.rb +28 -0
- data/spec/{lib → views/lib}/action_view/helpers/instance_tag_spec.rb +28 -22
- data/spec/views/lib/disabled_for_spec.rb +101 -0
- data/spec/{lib/builder_spec.rb → views/lib/form_builder_spec.rb} +5 -12
- data/spec/{lib → views/lib}/param_parser_spec.rb +10 -4
- metadata +66 -32
- data/lib/bot-away/action_controller/request.rb +0 -19
- data/spec/support/controllers/test_controller.rb +0 -18
data/lib/bot-away.rb
CHANGED
@@ -1,29 +1,44 @@
|
|
1
1
|
$:.unshift(File.dirname(__FILE__)) unless
|
2
2
|
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
3
|
|
4
|
-
require 'rubygems' unless defined?(Gem)
|
5
4
|
require 'action_controller'
|
6
5
|
require 'action_view'
|
7
6
|
require 'sc-core-ext'
|
8
7
|
|
9
8
|
require 'bot-away/param_parser'
|
10
|
-
require 'bot-away/
|
9
|
+
require 'bot-away/action_dispatch/request'
|
11
10
|
require 'bot-away/action_view/helpers/instance_tag'
|
12
11
|
require 'bot-away/spinner'
|
13
12
|
|
14
13
|
module BotAway
|
15
|
-
VERSION =
|
14
|
+
VERSION = File.read(File.join(File.dirname(__FILE__), "../VERSION")) unless defined?(VERSION)
|
16
15
|
|
17
16
|
class << self
|
18
17
|
attr_accessor :show_honeypots, :dump_params
|
19
18
|
|
20
|
-
def unfiltered_params(*
|
21
|
-
|
19
|
+
def unfiltered_params(*keys)
|
20
|
+
unfiltered_params = instance_variable_get("@unfiltered_params") || instance_variable_set("@unfiltered_params", [])
|
21
|
+
unfiltered_params.concat keys.flatten.collect { |k| k.to_s }
|
22
|
+
unfiltered_params
|
22
23
|
end
|
24
|
+
|
25
|
+
alias_method :accepts_unfiltered_params, :unfiltered_params
|
23
26
|
|
24
|
-
|
27
|
+
# options include:
|
28
|
+
# :controller
|
29
|
+
# :action
|
30
|
+
# :object_name
|
31
|
+
# :method_name
|
32
|
+
#
|
33
|
+
# excluded? will also check the current Rails run mode against disabled_for[:mode]
|
34
|
+
def excluded?(options)
|
35
|
+
options = options.stringify_keys
|
36
|
+
nonparams = options.stringify_keys.without('object_name', 'method_name')
|
37
|
+
(options['object_name'] && options['method_name'] &&
|
38
|
+
unfiltered_params_include?(options['object_name'], options['method_name'])) || disabled_for?(nonparams)
|
39
|
+
end
|
25
40
|
|
26
|
-
def
|
41
|
+
def unfiltered_params_include?(object_name, method_name)
|
27
42
|
unfiltered_params.collect! { |u| u.to_s }
|
28
43
|
if (object_name &&
|
29
44
|
(unfiltered_params.include?(object_name.to_s) ||
|
@@ -34,7 +49,52 @@ module BotAway
|
|
34
49
|
false
|
35
50
|
end
|
36
51
|
end
|
52
|
+
|
53
|
+
# Returns true if the given options match the options set via #disabled_for, or if the Rails run mode
|
54
|
+
# matches any run modes set via #disabled_for.
|
55
|
+
def disabled_for?(options)
|
56
|
+
return false if @disabled_for.nil? || options.empty?
|
57
|
+
options = options.stringify_keys
|
58
|
+
# p options
|
59
|
+
# p "===="
|
60
|
+
@disabled_for.each do |set|
|
61
|
+
if set.key?('mode')
|
62
|
+
next unless ENV['RAILS_ENV'] == set['mode'].to_s
|
63
|
+
return true if set.keys.length == 1
|
64
|
+
# if there are more keys, then it looks something like:
|
65
|
+
# disabled_for :mode => 'development', :controller => 'tests'
|
66
|
+
# and that means we need to check the next few conditions.
|
67
|
+
end
|
68
|
+
|
69
|
+
# p set
|
70
|
+
if set.key?('controller') && set.key?('action')
|
71
|
+
return true if set['controller'] == options['controller'] && set['action'] == options['action']
|
72
|
+
elsif set.key?('controller') && !set.key?('action')
|
73
|
+
return true if set['controller'] == options['controller']
|
74
|
+
elsif set.key?('action')
|
75
|
+
return true if set['action'] == options['action']
|
76
|
+
end
|
77
|
+
end
|
78
|
+
false
|
79
|
+
end
|
80
|
+
|
81
|
+
def disabled_for(options = {})
|
82
|
+
@disabled_for ||= []
|
83
|
+
if !options.empty?
|
84
|
+
@disabled_for << options.stringify_keys
|
85
|
+
end
|
86
|
+
@disabled_for
|
87
|
+
end
|
88
|
+
|
89
|
+
def reset!
|
90
|
+
self.show_honeypots = false
|
91
|
+
self.dump_params = false
|
92
|
+
self.unfiltered_params.clear
|
93
|
+
self.disabled_for.clear
|
94
|
+
end
|
37
95
|
end
|
96
|
+
|
97
|
+
delegate :accepts_unfiltered_params, :unfiltered_params, :to => :"self.class"
|
38
98
|
end
|
39
99
|
|
40
100
|
# WHY do I have to do this???
|
@@ -0,0 +1,20 @@
|
|
1
|
+
request = (defined?(Rails::VERSION) && Rails::VERSION::STRING >= "3.0") ?
|
2
|
+
ActionDispatch::Request : # Rails 3.0
|
3
|
+
ActionController::Request # Rails 2.3
|
4
|
+
|
5
|
+
request.module_eval do
|
6
|
+
def parameters_with_deobfuscation
|
7
|
+
# NFC what is happening behind the scenes but Rails 2.3 croaks when we memoize; Rails 3 croaks when we don't.
|
8
|
+
if defined?(Rails::VERSION) && Rails::VERSION::STRING >= "3.0"
|
9
|
+
@deobfuscated_parameters ||= begin
|
10
|
+
BotAway::ParamParser.new(ip, parameters_without_deobfuscation.dup).params
|
11
|
+
end
|
12
|
+
else
|
13
|
+
Rails.logger.info parameters_without_deobfuscation.inspect
|
14
|
+
BotAway::ParamParser.new(ip, parameters_without_deobfuscation.dup).params
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
alias_method_chain :parameters, :deobfuscation
|
19
|
+
alias_method :params, :parameters
|
20
|
+
end
|
@@ -3,8 +3,11 @@ class ActionView::Helpers::InstanceTag
|
|
3
3
|
|
4
4
|
def initialize_with_spinner(object_name, method_name, template_object, object = nil)
|
5
5
|
initialize_without_spinner(object_name, method_name, template_object, object)
|
6
|
-
|
7
|
-
|
6
|
+
|
7
|
+
if template_object.controller.send(:protect_against_forgery?) &&
|
8
|
+
!BotAway.excluded?(:object_name => object_name, :method_name => method_name) &&
|
9
|
+
!BotAway.excluded?(:controller => template_object.controller.controller_name,
|
10
|
+
:action => template_object.controller.action_name)
|
8
11
|
@spinner = BotAway::Spinner.new(template_object.request.ip, object_name, template_object.form_authenticity_token)
|
9
12
|
end
|
10
13
|
end
|
@@ -48,12 +51,18 @@ class ActionView::Helpers::InstanceTag
|
|
48
51
|
end
|
49
52
|
|
50
53
|
# Special case
|
51
|
-
def to_label_tag_with_obfuscation(text = nil, options = {})
|
54
|
+
def to_label_tag_with_obfuscation(text = nil, options = {}, &block)
|
52
55
|
# TODO: Can this be simplified? It's pretty similar to to_label_tag_without_obfuscation...
|
53
56
|
options = options.stringify_keys
|
54
57
|
tag_value = options.delete("value")
|
55
58
|
name_and_id = options.dup
|
56
|
-
|
59
|
+
|
60
|
+
if name_and_id["for"]
|
61
|
+
name_and_id["id"] = name_and_id["for"]
|
62
|
+
else
|
63
|
+
name_and_id.delete("id")
|
64
|
+
end
|
65
|
+
|
57
66
|
add_default_name_and_id_for_value(tag_value, name_and_id)
|
58
67
|
options["for"] ||= name_and_id["id"]
|
59
68
|
options["for"] = spinner.encode(options["for"]) if spinner && options["for"]
|
@@ -2,34 +2,47 @@ module BotAway
|
|
2
2
|
class ParamParser
|
3
3
|
attr_reader :params, :ip, :authenticity_token
|
4
4
|
|
5
|
-
def initialize(ip, params, authenticity_token =
|
5
|
+
def initialize(ip, params, authenticity_token = nil)
|
6
|
+
params = params.with_indifferent_access if !params.kind_of?(HashWithIndifferentAccess)
|
7
|
+
authenticity_token ||= params[:authenticity_token]
|
6
8
|
@ip, @params, @authenticity_token = ip, params, authenticity_token
|
7
|
-
|
9
|
+
|
10
|
+
if BotAway.dump_params
|
11
|
+
Rails.logger.debug("[BotAway] IP: #{@ip}")
|
12
|
+
Rails.logger.debug("[BotAway] Authenticity token: #{@authenticity_token}")
|
13
|
+
Rails.logger.debug("[BotAway] Parameters: #{params.inspect}")
|
14
|
+
end
|
15
|
+
|
8
16
|
if authenticity_token
|
9
17
|
if catch(:bastard) { deobfuscate! } == :took_the_bait
|
10
|
-
params.clear
|
18
|
+
#params.clear
|
19
|
+
# don't clear the controller or action keys, as Rails 3 needs them
|
20
|
+
params.keys.each { |key| params.delete(key) unless %w(controller action).include?(key) }
|
11
21
|
params[:suspected_bot] = true
|
12
22
|
end
|
13
23
|
end
|
14
24
|
end
|
15
25
|
|
16
26
|
def deobfuscate!(current = params, object_name = nil)
|
27
|
+
return current if BotAway.excluded?(:controller => params[:controller], :action => params[:action])
|
28
|
+
|
17
29
|
if object_name
|
18
30
|
spinner = BotAway::Spinner.new(ip, object_name, authenticity_token)
|
19
31
|
end
|
20
32
|
|
21
33
|
current.each do |key, value|
|
22
|
-
if object_name && !value.kind_of?(Hash) && !BotAway.excluded?(object_name, key)
|
23
|
-
if value.blank? && params.keys.include?(spun_key = spinner.encode("#{object_name}[#{key}]"))
|
24
|
-
current[key] = params.delete(spun_key)
|
25
|
-
else
|
26
|
-
#puts "throwing on #{object_name}[#{key}] because its not blank" if !value.blank?
|
27
|
-
#puts "throwing on #{object_name}[#{key}] because its not found" if defined?(spun_key) && !spun_key.nil?
|
28
|
-
throw :bastard, :took_the_bait
|
29
|
-
end
|
30
|
-
end
|
31
34
|
if value.kind_of?(Hash)
|
32
35
|
deobfuscate!(value, object_name ? "#{object_name}[#{key}]" : key)
|
36
|
+
else
|
37
|
+
if object_name && !BotAway.excluded?(:object_name => object_name, :method_name => key)
|
38
|
+
if value.blank? && params.keys.include?(spun_key = spinner.encode("#{object_name}[#{key}]"))
|
39
|
+
current[key] = params.delete(spun_key)
|
40
|
+
else
|
41
|
+
#puts "throwing on #{object_name}[#{key}] because its not blank" if !value.blank?
|
42
|
+
#puts "throwing on #{object_name}[#{key}] because its not found" if defined?(spun_key) && !spun_key.nil?
|
43
|
+
throw :bastard, :took_the_bait
|
44
|
+
end
|
45
|
+
end
|
33
46
|
end
|
34
47
|
end
|
35
48
|
end
|
data/lib/bot-away/spinner.rb
CHANGED
@@ -1,41 +1,69 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe TestController do
|
4
|
-
|
4
|
+
if RAILS_VERSION < "3.0"
|
5
|
+
# rails 2
|
6
|
+
|
7
|
+
def setup_default_controller
|
8
|
+
@request = ActionController::TestRequest.new
|
9
|
+
# can the authenticity token so that we can predict the generated element names
|
10
|
+
@request.session[:_csrf_token] = 'aVjGViz+pIphXt2pxrWfXgRXShOI0KXOILR23yw0WBo='
|
11
|
+
@request.remote_addr = '208.77.188.166' # example.com
|
12
|
+
@response = ActionController::TestResponse.new
|
13
|
+
@controller = TestController.new
|
14
|
+
|
15
|
+
@controller.request = @request
|
16
|
+
@controller.params = {}
|
17
|
+
@controller.send(:initialize_current_url)
|
18
|
+
@controller
|
19
|
+
end
|
20
|
+
|
21
|
+
include ActionController::TestProcess
|
22
|
+
|
23
|
+
def controller
|
24
|
+
@controller
|
25
|
+
end
|
26
|
+
else
|
27
|
+
# rails 3
|
28
|
+
def setup_default_controller
|
29
|
+
@controller.request.session[:_csrf_token] = 'aVjGViz+pIphXt2pxrWfXgRXShOI0KXOILR23yw0WBo='
|
30
|
+
@controller.request.remote_addr = '208.77.188.166'
|
31
|
+
|
32
|
+
@controller
|
33
|
+
end
|
34
|
+
|
35
|
+
#extend ActiveSupport::Concern
|
36
|
+
#include ActionController::TestCase::Behavior
|
37
|
+
delegate :session, :to => :controller
|
38
|
+
end
|
5
39
|
|
6
40
|
def prepare!(action = 'index', method = 'get')
|
7
|
-
|
8
|
-
@controller.params = {}
|
9
|
-
@controller.send(:initialize_current_url)
|
41
|
+
controller
|
10
42
|
send(method, action)
|
11
|
-
if
|
12
|
-
|
43
|
+
if RAILS_VERSION < "3.0"
|
44
|
+
if @response.template.instance_variable_get("@exception")
|
45
|
+
raise @response.template.instance_variable_get("@exception")
|
46
|
+
end
|
13
47
|
end
|
14
48
|
end
|
15
|
-
|
16
|
-
before :each do
|
17
|
-
@request = ActionController::TestRequest.new
|
18
|
-
# can the authenticity token so that we can predict the generated element names
|
19
|
-
@request.session[:_csrf_token] = 'aVjGViz+pIphXt2pxrWfXgRXShOI0KXOILR23yw0WBo='
|
20
|
-
@request.remote_addr = '208.77.188.166' # example.com
|
21
|
-
@response = ActionController::TestResponse.new
|
22
|
-
@controller = TestController.new
|
23
|
-
end
|
24
49
|
|
50
|
+
before(:each) { setup_default_controller }
|
51
|
+
|
25
52
|
after :each do
|
26
53
|
# effectively disables forgery protection.
|
27
54
|
TestController.request_forgery_protection_token = nil
|
28
55
|
end
|
56
|
+
|
29
57
|
|
30
58
|
context "with a model" do
|
31
59
|
context "with forgery protection" do
|
32
60
|
before :each do
|
33
|
-
(class <<
|
61
|
+
(class << controller; self; end).send(:protect_from_forgery)
|
34
62
|
prepare!('model_form')
|
35
63
|
end
|
36
64
|
|
37
65
|
it "should work?" do
|
38
|
-
puts @response.body
|
66
|
+
#puts @response.body
|
39
67
|
end
|
40
68
|
end
|
41
69
|
|
@@ -45,15 +73,15 @@ describe TestController do
|
|
45
73
|
end
|
46
74
|
|
47
75
|
it "should work?" do
|
48
|
-
puts @response.body
|
76
|
+
#puts @response.body
|
49
77
|
end
|
50
78
|
end
|
51
79
|
end
|
52
80
|
|
53
81
|
context "with forgery protection" do
|
54
82
|
before :each do
|
55
|
-
(class <<
|
56
|
-
prepare!
|
83
|
+
(class << controller; self; end).send(:protect_from_forgery)
|
84
|
+
prepare! if RAILS_VERSION < "3.0"
|
57
85
|
end
|
58
86
|
#"object_name_method_name" name="object_name[method_name]" size="30" type="text" value="" /></div>
|
59
87
|
#<input id="e21372563297c728093bf74c3cb6b96c" name="a0844d45bf150668ff1d86a6eb491969" size="30" type="text" value="method_value" />
|
@@ -64,30 +92,48 @@ describe TestController do
|
|
64
92
|
'842d8d1c80014ce9f3d974614338605c' => 'some_value'
|
65
93
|
}
|
66
94
|
post 'proc_form', form
|
67
|
-
puts @response.body
|
68
|
-
|
95
|
+
#puts @response.body
|
96
|
+
controller.params[:object_name].should == { 'method_name' => 'some_value' }
|
97
|
+
end
|
98
|
+
|
99
|
+
context "after processing valid obfuscated post" do
|
100
|
+
before(:each) do
|
101
|
+
post 'proc_form', { 'authenticity_token' => '1234',
|
102
|
+
'object_name' => { 'method_name' => '' },
|
103
|
+
'842d8d1c80014ce9f3d974614338605c' => 'some_value'
|
104
|
+
}
|
105
|
+
end
|
106
|
+
it "should allow params to be changed" do
|
107
|
+
# Whether it's best practice or not, the Rails params hash can normally be modified from the controller.
|
108
|
+
# So, it makes sense to verify that BotAway doesn't change this.
|
109
|
+
controller.params[:object_name][:method_name] = "a different value"
|
110
|
+
controller.params[:object_name][:method_name].should == "a different value"
|
111
|
+
|
112
|
+
controller.params.clear
|
113
|
+
controller.params.should == {}
|
114
|
+
end
|
69
115
|
end
|
70
116
|
|
71
117
|
it "drops invalid obfuscated form post" do
|
72
118
|
form = { 'authenticity_token' => '1234',
|
73
|
-
'object_name' => { 'method_name' => '
|
119
|
+
'object_name' => { 'method_name' => 'a bot filled this in' },
|
74
120
|
'842d8d1c80014ce9f3d974614338605c' => 'some_value'
|
75
121
|
}
|
76
122
|
post 'proc_form', form
|
77
|
-
|
78
|
-
|
123
|
+
controller.params['suspected_bot'].should == true
|
124
|
+
controller.params.keys.should_not include('object_name')
|
79
125
|
end
|
80
126
|
|
81
127
|
it "processes no params" do
|
82
128
|
post 'proc_form', { 'authenticity_token' => '1234' }
|
83
|
-
|
129
|
+
controller.params['suspected_bot'].should_not == true
|
84
130
|
end
|
85
131
|
|
86
132
|
it "should not fail on unfiltered params" do
|
87
|
-
|
133
|
+
BotAway.accepts_unfiltered_params :role_ids
|
88
134
|
@request.remote_addr = '127.0.0.1'
|
89
135
|
post 'proc_form', {'authenticity_token' => '1234', 'user' => { 'role_ids' => [1, 2] }}
|
90
|
-
|
136
|
+
controller.params['suspected_bot'].should_not == true
|
91
137
|
end
|
92
138
|
|
93
139
|
it "does not drop valid authentication request" do
|
@@ -103,10 +149,10 @@ describe TestController do
|
|
103
149
|
'4b9bab79bc1b1cd5229041c357750e0c' => 'pwpwpw',
|
104
150
|
'256307a36284445cc84014dae651f2ed' => '1'
|
105
151
|
}
|
106
|
-
|
152
|
+
controller.request.remote_addr = '127.0.0.1'
|
107
153
|
post 'proc_form', form
|
108
|
-
puts
|
109
|
-
|
154
|
+
# puts controller.params.inspect
|
155
|
+
controller.params.should == { 'action' => 'proc_form', 'controller' => 'test',
|
110
156
|
'authenticity_token' => 'yPgTAsngzpBO8k1v83RGH26sTrQYD50Ou2oiMT4r/iw=',
|
111
157
|
'user_session' => {
|
112
158
|
'login' => 'admin',
|
@@ -120,7 +166,7 @@ describe TestController do
|
|
120
166
|
|
121
167
|
context "without forgery protection" do
|
122
168
|
before :each do
|
123
|
-
prepare!
|
169
|
+
prepare! if RAILS_VERSION < "3.0"
|
124
170
|
end
|
125
171
|
|
126
172
|
it "processes non-obfuscated form post" do
|
@@ -128,12 +174,13 @@ describe TestController do
|
|
128
174
|
'object_name' => { 'method_name' => 'test' }
|
129
175
|
}
|
130
176
|
post 'proc_form', form
|
131
|
-
puts @response.body
|
132
|
-
|
133
|
-
|
177
|
+
# puts @response.body
|
178
|
+
controller.params.should_not == { 'suspected_bot' => true }
|
179
|
+
controller.params[:object_name].should == { 'method_name' => 'test' }
|
134
180
|
end
|
135
181
|
|
136
182
|
it "produces non-obfuscated form elements" do
|
183
|
+
prepare! if RAILS_VERSION >= "3.0"
|
137
184
|
@response.body.should_not match(/<\/div><input/)
|
138
185
|
end
|
139
186
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
unless defined?(RSPEC_VERSION)
|
2
|
+
begin
|
3
|
+
# RSpec 1.3.0
|
4
|
+
require 'spec/rake/spectask'
|
5
|
+
require 'spec/version'
|
6
|
+
|
7
|
+
RSPEC_VERSION = Spec::VERSION::STRING
|
8
|
+
rescue LoadError
|
9
|
+
# RSpec 2.0
|
10
|
+
begin
|
11
|
+
require 'rspec/core/rake_task'
|
12
|
+
require 'rspec/core/version'
|
13
|
+
|
14
|
+
RSPEC_VERSION = RSpec::Core::Version::STRING
|
15
|
+
rescue LoadError
|
16
|
+
raise "RSpec does not seem to be installed. You must install rspec to test this gem."
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|