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.
@@ -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/action_controller/request'
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 = '1.1.0'
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(*args)
21
- ActionController::Request.unfiltered_params(*args)
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
- alias accepts_unfiltered_params unfiltered_params
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 excluded?(object_name, method_name)
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
- if template_object.controller.send(:protect_against_forgery?) && !BotAway.excluded?(object_name, method_name)
7
- puts "#{object_name.inspect} => #{method_name.inspect}"
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
- name_and_id["id"] = name_and_id["for"]
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 = 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
- Rails.logger.debug(params.inspect) if BotAway.dump_params
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
@@ -7,9 +7,7 @@ class BotAway::Spinner
7
7
  key.to_s,
8
8
  secret)
9
9
 
10
- #puts secret
11
10
  @spinner = Digest::MD5.hexdigest(secret)
12
- #puts @spinner
13
11
  end
14
12
 
15
13
  def spinner
@@ -1,41 +1,69 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe TestController do
4
- include ActionController::TestProcess
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
- @controller.request = @request
8
- @controller.params = {}
9
- @controller.send(:initialize_current_url)
41
+ controller
10
42
  send(method, action)
11
- if @response.template.instance_variable_get("@exception")
12
- raise @response.template.instance_variable_get("@exception")
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 << @controller; self; end).send(:protect_from_forgery)
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 << @controller; self; end).send(:protect_from_forgery)
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
- @response.template.controller.params[:object_name].should == { 'method_name' => 'some_value' }
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' => 'test' },
119
+ 'object_name' => { 'method_name' => 'a bot filled this in' },
74
120
  '842d8d1c80014ce9f3d974614338605c' => 'some_value'
75
121
  }
76
122
  post 'proc_form', form
77
- puts @response.body
78
- @response.template.controller.params.should == { 'suspected_bot' => true }
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
- @response.template.controller.params.should_not == { 'suspected_bot' => true }
129
+ controller.params['suspected_bot'].should_not == true
84
130
  end
85
131
 
86
132
  it "should not fail on unfiltered params" do
87
- ActionController::Request.accepts_unfiltered_params :role_ids
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
- @response.template.controller.params.should_not == { 'suspected_bot' => true }
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
- @request.remote_addr = '127.0.0.1'
152
+ controller.request.remote_addr = '127.0.0.1'
107
153
  post 'proc_form', form
108
- puts @response.template.controller.params.inspect
109
- @response.template.controller.params.should == { 'action' => 'proc_form', 'controller' => 'test',
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
- @response.template.controller.params.should_not == { 'suspected_bot' => true }
133
- @response.template.controller.params[:object_name].should == { 'method_name' => 'test' }
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