bot-away 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|