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.
@@ -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