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/spec/spec_helper.rb
CHANGED
@@ -1,11 +1,112 @@
|
|
1
|
+
ENV['RAILS_ENV'] ||= 'test'
|
2
|
+
|
1
3
|
require 'rubygems'
|
2
4
|
require 'action_controller'
|
3
5
|
require 'action_view'
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
+
begin
|
8
|
+
# rails 2.x
|
9
|
+
ActionController::Routing::Routes.load!
|
10
|
+
ActionController::Base.session = { :key => "_myapp_session", :secret => "12345"*6 }
|
11
|
+
ActionController::Base.view_paths << File.expand_path(File.join(File.dirname(__FILE__), "support/views"))
|
12
|
+
RAILS_VERSION = "2.3"
|
13
|
+
rescue
|
14
|
+
# rails 3
|
15
|
+
#require 'active_record/railtie'
|
16
|
+
require 'action_controller/railtie'
|
17
|
+
require 'action_mailer/railtie'
|
18
|
+
require 'active_resource/railtie'
|
19
|
+
require 'rails/test_unit/railtie'
|
20
|
+
|
21
|
+
# stub out the csrf token so that it always produces a predictable (testable) result
|
22
|
+
module ActionView
|
23
|
+
# = Action View CSRF Helper
|
24
|
+
module Helpers
|
25
|
+
module CsrfHelper
|
26
|
+
# Returns a meta tag with the cross-site request forgery protection token
|
27
|
+
# for forms to use. Place this in your head.
|
28
|
+
def csrf_meta_tag
|
29
|
+
if protect_against_forgery?
|
30
|
+
%(<meta name="csrf-param" content="#{Rack::Utils.escape_html(request_forgery_protection_token)}"/>\n<meta name="csrf-token" content="1234"/>).html_safe
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class BotAwayApp < Rails::Application
|
38
|
+
config.session_store :cookie_store, :key => "_myapp_session", :secret => "12345"*6
|
39
|
+
paths.app.views = File.expand_path(File.join(File.dirname(__FILE__), "support/views"))
|
40
|
+
config.active_support.deprecation = :stderr
|
41
|
+
end
|
42
|
+
BotAwayApp.initialize!
|
43
|
+
BotAwayApp.routes.draw do
|
44
|
+
match '/:controller/:action'
|
45
|
+
end
|
46
|
+
|
47
|
+
RAILS_VERSION = "3.0"
|
48
|
+
end
|
7
49
|
|
8
50
|
require File.join(File.dirname(__FILE__), '../lib/bot-away')
|
51
|
+
require File.join(File.dirname(__FILE__), "rspec_version")
|
52
|
+
|
53
|
+
class MockObject
|
54
|
+
attr_accessor :method_name
|
55
|
+
|
56
|
+
def id
|
57
|
+
1
|
58
|
+
end
|
59
|
+
|
60
|
+
# for testing grouped_collection_select
|
61
|
+
def object_name
|
62
|
+
[self]
|
63
|
+
end
|
64
|
+
|
65
|
+
def initialize
|
66
|
+
@method_name = 'method_value'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
if RSPEC_VERSION < "2.0"
|
72
|
+
Spec::Runner.configure do |config|
|
73
|
+
config.before(:each) do
|
74
|
+
BotAway.reset!
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
if defined?(ActionController::TestProcess)
|
79
|
+
# note that this only matters for TEST requests; real ones use the params parser.
|
80
|
+
module ActionController::TestProcess
|
81
|
+
def process_with_deobfuscation(action, parameters = nil, session = nil, flash = nil, http_method = 'GET')
|
82
|
+
process_without_deobfuscation(action, parameters, session, flash, http_method)
|
83
|
+
@request.parameters.replace(BotAway::ParamParser.new(@request.ip, @request.params).params)
|
84
|
+
end
|
85
|
+
|
86
|
+
alias_method_chain :process, :deobfuscation
|
87
|
+
end
|
88
|
+
end
|
89
|
+
else
|
90
|
+
require 'rspec/rails'
|
91
|
+
|
92
|
+
# note that this only matters for TEST requests; real ones use the params parser.
|
93
|
+
module ActionController::TestCase::Behavior
|
94
|
+
def process_with_deobfuscation(action, parameters = nil, session = nil, flash = nil, http_method = 'GET')
|
95
|
+
process_without_deobfuscation(action, parameters, session, flash, http_method)
|
96
|
+
request.parameters.replace(BotAway::ParamParser.new(request.ip, request.params).params)
|
97
|
+
end
|
98
|
+
|
99
|
+
alias_method_chain :process, :deobfuscation
|
100
|
+
end
|
101
|
+
|
102
|
+
RSpec.configure do |config|
|
103
|
+
config.before(:each) do
|
104
|
+
BotAway.reset!
|
105
|
+
# BotAway doesn't work without forgery protection, and RSpec-Rails 2 disables it. Lost way too many hours on this.
|
106
|
+
ActionController::Base.allow_forgery_protection = true
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
9
110
|
|
10
111
|
Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each do |fi|
|
11
112
|
require fi
|
@@ -1,68 +1,123 @@
|
|
1
1
|
module ObfuscationHelper
|
2
|
-
def
|
3
|
-
|
4
|
-
subject.should include_honeypot(object_name, method_name)
|
5
|
-
end
|
6
|
-
end
|
7
|
-
|
8
|
-
def is_obfuscated_as(id, name)
|
9
|
-
it "is obfuscated as #{id}, #{name}" do
|
10
|
-
subject.should be_obfuscated_as(id, name)
|
11
|
-
end
|
2
|
+
def controller_class
|
3
|
+
TestController
|
12
4
|
end
|
13
5
|
|
14
6
|
def dump
|
15
|
-
|
7
|
+
result = yield
|
8
|
+
puts result if ENV['DUMP']
|
9
|
+
result
|
16
10
|
end
|
17
|
-
|
11
|
+
|
12
|
+
if RAILS_VERSION >= "3.0"
|
13
|
+
def self.included(base)
|
14
|
+
base.before(:each) do
|
15
|
+
session[:_csrf_token] = '1234'
|
16
|
+
controller.stub!(:protect_from_forgery?).and_return(true)
|
17
|
+
if respond_to?(:view)
|
18
|
+
controller.request.path_parameters["action"] ||= "index"
|
19
|
+
controller.action_name ||= "index"
|
20
|
+
view.stub!(:request_forgery_protection_token).and_return(:authenticity_token)
|
21
|
+
view.stub!(:form_authenticity_token).and_return('1234')
|
22
|
+
else
|
23
|
+
# response.stub!(:request_forgery_protection_token).and_return(:authenticity_token)
|
24
|
+
# response.stub!(:form_authenticity_token).and_return('1234')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
18
30
|
def builder
|
19
31
|
return @builder if @builder
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
if !options.kind_of?(Hash)
|
29
|
-
options = { :obfuscated_id => options, :obfuscated_name => unused }
|
30
|
-
end
|
31
|
-
|
32
|
-
obfuscated_id = options[:obfuscated_id] || self.obfuscated_id
|
33
|
-
obfuscated_name = options[:obfuscatd_name] || self.obfuscated_name
|
34
|
-
|
35
|
-
value = yield
|
36
|
-
context "##{method}" do
|
37
|
-
subject { proc { dump { value } } }
|
38
|
-
|
39
|
-
if options[:name]
|
40
|
-
includes_honeypot(options[:name], nil)
|
41
|
-
else
|
42
|
-
includes_honeypot(options[:object_name] || object_name, options[:method_name] || method_name)
|
43
|
-
end
|
44
|
-
is_obfuscated_as(obfuscated_id, obfuscated_name)
|
32
|
+
if RAILS_VERSION < "3.0"
|
33
|
+
response = TestController.call(Rack::MockRequest.env_for('/').merge({'REQUEST_URI' => '/',
|
34
|
+
'REMOTE_ADDR' => '127.0.0.1'}))
|
35
|
+
response.template.controller.request_forgery_protection_token = :authenticity_token
|
36
|
+
response.template.controller.session[:_csrf_token] = '1234'
|
37
|
+
@builder = ActionView::Helpers::FormBuilder.new(:object_name, MockObject.new, response.template, {}, proc {})
|
38
|
+
else
|
39
|
+
@builder = ActionView::Base.default_form_builder.new(:object_name, MockObject.new, view, {}, proc {})
|
45
40
|
end
|
46
41
|
end
|
47
|
-
|
42
|
+
|
43
|
+
# this is the obfuscated version of the string "object_name_method_name"
|
48
44
|
def obfuscated_id
|
49
|
-
|
45
|
+
self.class.obfuscated_id
|
50
46
|
end
|
51
|
-
|
47
|
+
|
48
|
+
# this is the obfuscated version of the string "object_name[method_name]"
|
52
49
|
def obfuscated_name
|
53
|
-
|
50
|
+
self.class.obfuscated_name
|
54
51
|
end
|
55
52
|
|
56
53
|
def object_name
|
57
|
-
|
54
|
+
self.class.object_name
|
58
55
|
end
|
59
|
-
|
56
|
+
|
60
57
|
def method_name
|
61
|
-
|
58
|
+
self.class.method_name
|
59
|
+
end
|
60
|
+
|
61
|
+
module ClassMethods
|
62
|
+
def includes_honeypot(object_name, method_name)
|
63
|
+
it "includes a honeypot called #{object_name}[#{method_name}]" do
|
64
|
+
subject.should include_honeypot(object_name, method_name)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def is_obfuscated_as(id, name)
|
69
|
+
it "is obfuscated as #{id}, #{name}" do
|
70
|
+
subject.should be_obfuscated_as(id, name)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def obfuscates(method, options = {}, unused = nil, &block)
|
75
|
+
if !options.kind_of?(Hash)
|
76
|
+
options = { :obfuscated_id => options, :obfuscated_name => unused }
|
77
|
+
end
|
78
|
+
|
79
|
+
obfuscated_id = options[:obfuscated_id] || self.obfuscated_id
|
80
|
+
obfuscated_name = options[:obfuscatd_name] || self.obfuscated_name
|
81
|
+
|
82
|
+
context "##{method}" do
|
83
|
+
before(:each) { @obfuscates_value = instance_eval(&block) }
|
84
|
+
subject { proc { dump { @obfuscates_value } } }
|
85
|
+
|
86
|
+
if options[:name]
|
87
|
+
includes_honeypot(options[:name], nil)
|
88
|
+
else
|
89
|
+
includes_honeypot(options[:object_name] || object_name, options[:method_name] || method_name)
|
90
|
+
end
|
91
|
+
is_obfuscated_as(obfuscated_id, obfuscated_name)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def obfuscated_id
|
96
|
+
RAILS_VERSION >= "3.0" ? "f51a02a636f507f1bd64722451b71297" : "e21372563297c728093bf74c3cb6b96c"
|
97
|
+
end
|
98
|
+
|
99
|
+
def obfuscated_name
|
100
|
+
RAILS_VERSION >= "3.0" ? "cd538a9170613d6dedbcc54a0aa24881" : "a0844d45bf150668ff1d86a6eb491969"
|
101
|
+
end
|
102
|
+
|
103
|
+
def object_name
|
104
|
+
"object_name"
|
105
|
+
end
|
106
|
+
|
107
|
+
def method_name
|
108
|
+
"method_name"
|
109
|
+
end
|
62
110
|
end
|
63
111
|
end
|
64
112
|
|
65
|
-
|
66
|
-
config
|
67
|
-
|
113
|
+
if RSPEC_VERSION < "2.0"
|
114
|
+
Spec::Runner.configure do |config|
|
115
|
+
config.extend ObfuscationHelper::ClassMethods
|
116
|
+
config.include ObfuscationHelper
|
117
|
+
end
|
118
|
+
else
|
119
|
+
RSpec.configure do |config|
|
120
|
+
config.extend ObfuscationHelper::ClassMethods
|
121
|
+
config.include ObfuscationHelper
|
122
|
+
end
|
68
123
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec/mocks'
|
2
|
+
|
3
|
+
module Rails
|
4
|
+
class << self
|
5
|
+
def logger
|
6
|
+
return @logger if @logger
|
7
|
+
@logger = Object.new
|
8
|
+
klass = class << @logger; self; end
|
9
|
+
if RSPEC_VERSION < "2.0"
|
10
|
+
klass.send(:include, Spec::Mocks::Methods)
|
11
|
+
else
|
12
|
+
end
|
13
|
+
|
14
|
+
def @logger.debug(message); end
|
15
|
+
def @logger.info(message); end
|
16
|
+
def @logger.error(message); end
|
17
|
+
def @logger.warn(message); end
|
18
|
+
@logger
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class Post
|
2
|
+
attr_reader :subject, :body, :subscribers
|
3
|
+
|
4
|
+
if defined?(ActiveModel)
|
5
|
+
extend ActiveModel::Naming
|
6
|
+
|
7
|
+
def to_key
|
8
|
+
[1]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class ApplicationController < ActionController::Base
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
class TestController < ApplicationController
|
18
|
+
def index
|
19
|
+
end
|
20
|
+
|
21
|
+
def model_form
|
22
|
+
@post = Post.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def proc_form
|
26
|
+
render :text => params.to_yaml
|
27
|
+
end
|
28
|
+
end
|
@@ -1,23 +1,29 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
3
|
+
describe ActionView::Helpers::InstanceTag do
|
4
|
+
if method_defined?(:controller)
|
5
|
+
def template
|
6
|
+
view
|
7
|
+
end
|
8
|
+
else
|
9
|
+
def template
|
10
|
+
return @response if @response
|
11
|
+
@response = TestController.call(Rack::MockRequest.env_for('/').merge({'REQUEST_URI' => '/',
|
12
|
+
'REMOTE_ADDR' => '127.0.0.1'}))
|
13
|
+
@response.template.controller.request_forgery_protection_token = :authenticity_token
|
14
|
+
@response.template.controller.session[:_csrf_token] = '1234'
|
15
|
+
@response.template
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def mock_object
|
20
|
+
@mock_object ||= MockObject.new
|
21
|
+
end
|
15
22
|
|
16
|
-
def default_instance_tag
|
17
|
-
|
18
|
-
end
|
23
|
+
def default_instance_tag
|
24
|
+
ActionView::Helpers::InstanceTag.new("object_name", "method_name", template, mock_object)
|
25
|
+
end
|
19
26
|
|
20
|
-
describe ActionView::Helpers::InstanceTag do
|
21
27
|
subject { default_instance_tag }
|
22
28
|
|
23
29
|
context "with a valid text area tag" do
|
@@ -39,17 +45,17 @@ describe ActionView::Helpers::InstanceTag do
|
|
39
45
|
end
|
40
46
|
|
41
47
|
it "should obfuscate tag name" do
|
42
|
-
subject.obfuscated_tag(*@tag_options).should =~ /name="
|
48
|
+
subject.obfuscated_tag(*@tag_options).should =~ /name="#{obfuscated_name}"/
|
43
49
|
end
|
44
50
|
|
45
51
|
it "should obfuscate tag id" do
|
46
|
-
subject.obfuscated_tag(*@tag_options).should =~ /id="
|
47
|
-
end
|
48
|
-
|
49
|
-
it "should not obfuscate tag value" do
|
50
|
-
subject.obfuscated_tag(*@tag_options).should_not =~ /value="5a6a50d5fd0b5c8b1190d87eb0057e47"/
|
52
|
+
subject.obfuscated_tag(*@tag_options).should =~ /id="#{obfuscated_id}"/
|
51
53
|
end
|
52
54
|
|
55
|
+
# it "should not obfuscate tag value" do
|
56
|
+
# subject.obfuscated_tag(*@tag_options).should =~ /value="@tag_options"/
|
57
|
+
# end
|
58
|
+
#
|
53
59
|
it "should include unobfuscated tag value" do
|
54
60
|
subject.obfuscated_tag(*@tag_options).should =~ /value="method_value"/
|
55
61
|
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Bot-Away" do
|
4
|
+
# Basically we're just switching BA on and off, so we only need 2 sets of tests: what to expect when it's on, and
|
5
|
+
# what to expect when it's off. The rest of this file just flips the switches.
|
6
|
+
|
7
|
+
def test_params
|
8
|
+
{"model"=>"User", "commit"=>"Sign in",
|
9
|
+
"authenticity_token"=>"XBQEDkXrm4E8U9slBX45TWNx7TPcx8ww2FSJRy/XXg4=",
|
10
|
+
"action"=>"index", "controller"=>"test", "user"=>{"username"=>"admin", "password"=>"Admin01"}
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.it_should_be_disabled
|
15
|
+
it "should not obfuscate the field, because it should be disabled" do
|
16
|
+
builder.text_field('method_name').should_not match(/name="#{obfuscated_name}/)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should not drop invalid params, because it should be disabled" do
|
20
|
+
parms = BotAway::ParamParser.new('127.0.0.1', test_params).params
|
21
|
+
parms.should == test_params
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should be disabled" do
|
25
|
+
BotAway.disabled_for?(:controller => 'test', :action => "index").should == true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.it_should_be_enabled
|
30
|
+
it "should obfuscate the field, because it should be enabled" do
|
31
|
+
builder.text_field('method_name').should be_obfuscated_as(obfuscated_id, obfuscated_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should drop invalid params, because it should be enabled" do
|
35
|
+
parms = BotAway::ParamParser.new('127.0.0.1', test_params).params
|
36
|
+
parms.should_not == test_params
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should be disabled" do
|
40
|
+
BotAway.disabled_for?(:controller => 'test', :action => "index").should == false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# flip-switching begins
|
45
|
+
|
46
|
+
context "with matching controller name" do
|
47
|
+
context "and no action" do
|
48
|
+
before(:each) { BotAway.disabled_for :controller => 'test' }
|
49
|
+
it_should_be_disabled
|
50
|
+
end
|
51
|
+
|
52
|
+
context "and matching action" do
|
53
|
+
before(:each) { BotAway.disabled_for :controller => 'test', :action => 'index' }
|
54
|
+
it_should_be_disabled
|
55
|
+
end
|
56
|
+
|
57
|
+
context "and not matching action" do
|
58
|
+
before(:each) { BotAway.disabled_for :controller => 'test', :action => 'create' }
|
59
|
+
it_should_be_enabled
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context "with not matching controller name" do
|
64
|
+
context "and no action" do
|
65
|
+
before(:each) { BotAway.disabled_for :controller => 'users' }
|
66
|
+
it_should_be_enabled
|
67
|
+
end
|
68
|
+
|
69
|
+
context "and matching action" do
|
70
|
+
before(:each) { BotAway.disabled_for :controller => 'users', :action => 'index' }
|
71
|
+
it_should_be_enabled
|
72
|
+
end
|
73
|
+
|
74
|
+
context "and not matching action" do
|
75
|
+
before(:each) { BotAway.disabled_for :controller => 'users', :action => 'create' }
|
76
|
+
it_should_be_enabled
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context "with no controller name" do
|
81
|
+
context "and matching action" do
|
82
|
+
before(:each) { BotAway.disabled_for :action => 'index' }
|
83
|
+
it_should_be_disabled
|
84
|
+
end
|
85
|
+
|
86
|
+
context "and not matching action" do
|
87
|
+
before(:each) { BotAway.disabled_for :action => 'create' }
|
88
|
+
it_should_be_enabled
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context "with matching mode" do
|
93
|
+
before(:each) { BotAway.disabled_for :mode => ENV['RAILS_ENV'] }
|
94
|
+
it_should_be_disabled
|
95
|
+
end
|
96
|
+
|
97
|
+
context "with not matching mode" do
|
98
|
+
before(:each) { BotAway.disabled_for :mode => "this_is_not_#{ENV['RAILS_ENV']}" }
|
99
|
+
it_should_be_enabled
|
100
|
+
end
|
101
|
+
end
|