bot-away 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/Manifest.txt +23 -0
- data/README.rdoc +88 -0
- data/Rakefile +45 -0
- data/lib/bot-away.rb +19 -0
- data/lib/bot-away/action_controller/request.rb +8 -0
- data/lib/bot-away/action_view/helpers/instance_tag.rb +92 -0
- data/lib/bot-away/param_parser.rb +36 -0
- data/lib/bot-away/spinner.rb +22 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/spec/controllers/test_controller_spec.rb +128 -0
- data/spec/lib/action_view/helpers/instance_tag_spec.rb +69 -0
- data/spec/lib/builder_spec.rb +48 -0
- data/spec/lib/param_parser_spec.rb +55 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/controllers/test_controller.rb +18 -0
- data/spec/support/honeypot_matcher.rb +26 -0
- data/spec/support/obfuscation_helper.rb +57 -0
- data/spec/support/obfuscation_matcher.rb +28 -0
- data/spec/support/views/test/index.html.erb +4 -0
- data/spec/support/views/test/model_form.html.erb +6 -0
- metadata +182 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
README.rdoc
|
4
|
+
Rakefile
|
5
|
+
lib/bot-away.rb
|
6
|
+
lib/bot-away/action_controller/request.rb
|
7
|
+
lib/bot-away/action_view/helpers/instance_tag.rb
|
8
|
+
lib/bot-away/param_parser.rb
|
9
|
+
lib/bot-away/spinner.rb
|
10
|
+
script/console
|
11
|
+
script/destroy
|
12
|
+
script/generate
|
13
|
+
spec/controllers/test_controller_spec.rb
|
14
|
+
spec/lib/action_view/helpers/instance_tag_spec.rb
|
15
|
+
spec/lib/builder_spec.rb
|
16
|
+
spec/lib/param_parser_spec.rb
|
17
|
+
spec/spec_helper.rb
|
18
|
+
spec/support/controllers/test_controller.rb
|
19
|
+
spec/support/honeypot_matcher.rb
|
20
|
+
spec/support/obfuscation_helper.rb
|
21
|
+
spec/support/obfuscation_matcher.rb
|
22
|
+
spec/support/views/test/index.html.erb
|
23
|
+
spec/support/views/test/model_form.html.erb
|
data/README.rdoc
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
= bot-away
|
2
|
+
|
3
|
+
* http://github.com/sinisterchipmunk/bot-away
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
Unobtrusively detects form submissions made by spambots, and silently drops those submissions. The key word here is
|
8
|
+
"unobtrusive" -- this is NOT a CAPTCHA. This is transparent, modular implementation of the bot-catching techniques
|
9
|
+
discussed by Ned Batchelder at http://nedbatchelder.com/text/stopbots.html
|
10
|
+
|
11
|
+
If a submission is detected, the params hash is cleared, so the data can't be used. Since this includes the authenticity
|
12
|
+
token, Rails should barf due to an invalid or missing authenticity token. Congrats, spam blocked.
|
13
|
+
|
14
|
+
The specifics of the techniques employed for filtering spambots are discussed Ned's site at the above location; however,
|
15
|
+
here's a brief run-down of what's going on:
|
16
|
+
|
17
|
+
* Your code stays the same. After the bot-away gem has been activated, all Rails-generated forms on your site
|
18
|
+
will automatically be transformed into bot-resistent forms.
|
19
|
+
* All of the form elements that you create (for instance, a "comment" model with a "body" field) are turned into
|
20
|
+
dummy elements, or honeypots, and are made invisible to the end user. This is done using div elements and inline CSS
|
21
|
+
stylesheets. There are several ways an element can be hidden, and these approaches are chosen at random to help
|
22
|
+
minimize predictability. In the rare event that a real user actually can see the element, it has a label next to it
|
23
|
+
along the lines of "Leave this blank" -- though the exact message is randomized to help prevent detection.
|
24
|
+
* All of the form elements are mirrored by hashes. The hashes are generated using the session's authenticity token,
|
25
|
+
so they can't be predicted.
|
26
|
+
* When data is submitted, bot-away steps in and 1.) validates that no honeypots have been filled in; and 2)
|
27
|
+
converts the hashed elements back into the field names that you are expecting (replacing the honeypot fields).
|
28
|
+
* If a honeypot has been filled in, or a hashed element is missing where it was expected, then the request is
|
29
|
+
considered to be either spam, or tampered with; and the entire params hash is emptied. Since this happens at the
|
30
|
+
lowest level, the most likely result is that Rails will complain that the user's authenticity token is invalid. If
|
31
|
+
that does not happen, then your code will be passed a params hash containing only a "suspected_bot" key, and an error
|
32
|
+
will result. Either way, the spambot has been foiled!
|
33
|
+
|
34
|
+
== FEATURES/PROBLEMS:
|
35
|
+
|
36
|
+
* Wherever protection from forgery is not enabled in your Rails app, the Rails forms will be generated as if this gem
|
37
|
+
did not exist. That means hashed elements won't be generated, honeypots won't be generated, and posted forms will not
|
38
|
+
be intercepted.
|
39
|
+
|
40
|
+
* By default, protection from forgery is enabled for all Rails controllers, so by default the above-mentioned checks
|
41
|
+
will also be triggered. For more details on forgery protection, see:
|
42
|
+
http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection/ClassMethods.html
|
43
|
+
|
44
|
+
* The techniques implemented by this library will be very difficult for a spambot to circumvent. However, keep in mind
|
45
|
+
that since the pages have to be machine-readable by definition, and since this gem has to follow certain protocols
|
46
|
+
in order to avoid confusing lots of humans (such as hiding the honeypots), it is always theoretically possible for
|
47
|
+
a spambot to get around it. It's just very, very difficult.
|
48
|
+
|
49
|
+
== REQUIREMENTS:
|
50
|
+
|
51
|
+
* Rails 2.3.5 or better.
|
52
|
+
|
53
|
+
== INSTALL:
|
54
|
+
|
55
|
+
* sudo gem install bot-away
|
56
|
+
|
57
|
+
== USAGE:
|
58
|
+
|
59
|
+
In your Rails config/environment.rb:
|
60
|
+
|
61
|
+
* config.gem 'bot-away'
|
62
|
+
|
63
|
+
That's it.
|
64
|
+
|
65
|
+
== LICENSE:
|
66
|
+
|
67
|
+
(The MIT License)
|
68
|
+
|
69
|
+
Copyright (c) 2010 Colin MacKenzie IV
|
70
|
+
|
71
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
72
|
+
a copy of this software and associated documentation files (the
|
73
|
+
'Software'), to deal in the Software without restriction, including
|
74
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
75
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
76
|
+
permit persons to whom the Software is furnished to do so, subject to
|
77
|
+
the following conditions:
|
78
|
+
|
79
|
+
The above copyright notice and this permission notice shall be
|
80
|
+
included in all copies or substantial portions of the Software.
|
81
|
+
|
82
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
83
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
84
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
85
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
86
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
87
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
88
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'hoe', '>= 2.1.0'
|
3
|
+
require 'hoe'
|
4
|
+
require 'fileutils'
|
5
|
+
require './lib/bot-away'
|
6
|
+
|
7
|
+
Hoe.plugin :newgem
|
8
|
+
# Hoe.plugin :website
|
9
|
+
# Hoe.plugin :cucumberfeatures
|
10
|
+
|
11
|
+
# Generate all the Rake tasks
|
12
|
+
# Run 'rake -T' to see list of generated tasks (from gem root directory)
|
13
|
+
$hoe = Hoe.spec 'bot-away' do
|
14
|
+
self.developer 'Colin MacKenzie IV', 'sinisterchipmunk@gmail.com'
|
15
|
+
self.extra_deps = [['actionpack','>= 2.3.5'],['sc-core-ext','>= 1.1.1']]
|
16
|
+
self.readme_file = "README.rdoc"
|
17
|
+
end
|
18
|
+
|
19
|
+
Rake::RDocTask.new(:docs) do |rdoc|
|
20
|
+
files = ['README.rdoc', # 'LICENSE', 'CHANGELOG',
|
21
|
+
'lib/**/*.rb', 'doc/**/*.rdoc']#, 'spec/*.rb']
|
22
|
+
rdoc.rdoc_files.add(files)
|
23
|
+
rdoc.main = 'README.rdoc'
|
24
|
+
rdoc.title = 'EVE Documentation'
|
25
|
+
#rdoc.template = '/path/to/gems/allison-2.0/lib/allison'
|
26
|
+
rdoc.rdoc_dir = 'doc'
|
27
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
require 'newgem/tasks'
|
32
|
+
Dir['tasks/**/*.rake'].each { |t| load t }
|
33
|
+
|
34
|
+
require 'spec/rake/spectask'
|
35
|
+
|
36
|
+
desc "Run all examples with RCov"
|
37
|
+
Spec::Rake::SpecTask.new('rcov') do |t|
|
38
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
39
|
+
t.rcov = true
|
40
|
+
t.rcov_opts = ['--exclude', 'spec,/home/*']
|
41
|
+
end
|
42
|
+
|
43
|
+
# TODO - want other tests/tasks run by default? Add them to the list
|
44
|
+
# remove_task :default
|
45
|
+
# task :default => [:spec, :features]
|
data/lib/bot-away.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
require 'rubygems' unless defined?(Gem)
|
5
|
+
require 'action_controller'
|
6
|
+
require 'action_view'
|
7
|
+
require 'sc-core-ext'
|
8
|
+
|
9
|
+
require 'bot-away/param_parser'
|
10
|
+
require 'bot-away/action_controller/request'
|
11
|
+
require 'bot-away/action_view/helpers/instance_tag'
|
12
|
+
require 'bot-away/spinner'
|
13
|
+
|
14
|
+
module BotAway
|
15
|
+
VERSION = '1.0.0'
|
16
|
+
end
|
17
|
+
|
18
|
+
# WHY do I have to do this???
|
19
|
+
ActionView::Base.send :include, ActionView::Helpers
|
@@ -0,0 +1,8 @@
|
|
1
|
+
class ActionController::Request < Rack::Request
|
2
|
+
def parameters_with_deobfuscation
|
3
|
+
@parameters ||= BotAway::ParamParser.new(ip, parameters_without_deobfuscation).params
|
4
|
+
end
|
5
|
+
|
6
|
+
alias_method_chain :parameters, :deobfuscation
|
7
|
+
alias_method :params, :parameters
|
8
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
class ActionView::Helpers::InstanceTag
|
2
|
+
attr_reader :spinner
|
3
|
+
|
4
|
+
def initialize_with_spinner(object_name, method_name, template_object, object = nil)
|
5
|
+
initialize_without_spinner(object_name, method_name, template_object, object)
|
6
|
+
if template_object.controller.send(:protect_against_forgery?)
|
7
|
+
@spinner = BotAway::Spinner.new(template_object.request.ip, object_name, template_object.form_authenticity_token)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def obfuscate_options(options)
|
12
|
+
add_default_name_and_id(options)
|
13
|
+
assuming(spinner && options) do
|
14
|
+
options['name'] &&= spinner.encode(options['name'])
|
15
|
+
options['id'] &&= spinner.encode(options['id'])
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def honeypot_options(options)
|
20
|
+
add_default_name_and_id(options)
|
21
|
+
assuming(spinner && options) do
|
22
|
+
options['value'] &&= ''
|
23
|
+
options['autocomplete'] = 'off'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def assuming(object)
|
28
|
+
yield if object
|
29
|
+
object
|
30
|
+
end
|
31
|
+
|
32
|
+
def honeypot_tag(name, options = nil, *args)
|
33
|
+
tag_without_honeypot(name, honeypot_options(options.dup? || {}), *args)
|
34
|
+
end
|
35
|
+
|
36
|
+
def obfuscated_tag(name, options = nil, *args)
|
37
|
+
tag_without_honeypot(name, obfuscate_options(options.dup? || {}), *args)
|
38
|
+
end
|
39
|
+
|
40
|
+
def tag_with_honeypot(name, options = nil, *args)
|
41
|
+
if spinner
|
42
|
+
obfuscated_tag(name, options, *args) + disguise(honeypot_tag(name, options, *args))
|
43
|
+
else
|
44
|
+
tag_without_honeypot(name, options, *args)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Special case
|
49
|
+
def to_label_tag_with_obfuscation(text = nil, options = {})
|
50
|
+
# TODO: Can this be simplified? It's pretty similar to to_label_tag_without_obfuscation...
|
51
|
+
options = options.stringify_keys
|
52
|
+
tag_value = options.delete("value")
|
53
|
+
name_and_id = options.dup
|
54
|
+
name_and_id["id"] = name_and_id["for"]
|
55
|
+
add_default_name_and_id_for_value(tag_value, name_and_id)
|
56
|
+
options["for"] ||= name_and_id["id"]
|
57
|
+
options["for"] = spinner.encode(options["for"]) if spinner && options["for"]
|
58
|
+
to_label_tag_without_obfuscation(text, options)
|
59
|
+
end
|
60
|
+
|
61
|
+
def content_tag_with_obfuscation(name, content_or_options_with_block = nil, options = nil, *args, &block)
|
62
|
+
if block_given?
|
63
|
+
content_tag_without_obfuscation(name, content_or_options_with_block, options, *args, &block)
|
64
|
+
else
|
65
|
+
# this should cover all Rails selects.
|
66
|
+
if spinner && options && (options.keys.include?('id') || options.keys.include?('name'))
|
67
|
+
disguise(content_tag_without_obfuscation(name, '', honeypot_options(options), *args)) +
|
68
|
+
content_tag_without_obfuscation(name, content_or_options_with_block, obfuscate_options(options), *args)
|
69
|
+
else
|
70
|
+
content_tag_without_obfuscation(name, content_or_options_with_block, options, *args)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
alias_method_chain :initialize, :spinner
|
76
|
+
alias_method_chain :tag, :honeypot
|
77
|
+
alias_method_chain :to_label_tag, :obfuscation
|
78
|
+
alias_method_chain :content_tag, :obfuscation
|
79
|
+
|
80
|
+
def disguise(element)
|
81
|
+
case rand(3)
|
82
|
+
when 0 # Hidden
|
83
|
+
"<div style='display:none;'>Leave this empty: #{element}</div>"
|
84
|
+
when 1 # Off-screen
|
85
|
+
"<div style='position:absolute;left:-1000px;top:-1000px;'>Don't fill this in: #{element}</div>"
|
86
|
+
when 2 # Negligible size
|
87
|
+
"<div style='position:absolute;width:0px;height:1px;z-index:-1;color:transparent;overflow:hidden;'>Keep this blank: #{element}</div>"
|
88
|
+
else # this should never happen?
|
89
|
+
disguise(element)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module BotAway
|
2
|
+
class ParamParser
|
3
|
+
class ObfuscationMissing < StandardError; end #:nodoc:
|
4
|
+
|
5
|
+
attr_reader :params, :ip, :authenticity_token
|
6
|
+
|
7
|
+
def initialize(ip, params, authenticity_token = params[:authenticity_token])
|
8
|
+
@ip, @params, @authenticity_token = ip, params, authenticity_token
|
9
|
+
if authenticity_token
|
10
|
+
if catch(:bastard) { deobfuscate! } == :took_the_bait
|
11
|
+
params.clear
|
12
|
+
params[:suspected_bot] = true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def deobfuscate!(current = params, object_name = nil)
|
18
|
+
if object_name
|
19
|
+
spinner = BotAway::Spinner.new(ip, object_name, authenticity_token)
|
20
|
+
end
|
21
|
+
|
22
|
+
current.each do |key, value|
|
23
|
+
if object_name
|
24
|
+
if value.blank? && params.keys.include?(spun_key = spinner.encode("#{object_name}[#{key}]"))
|
25
|
+
current[key] = params.delete(spun_key)
|
26
|
+
else
|
27
|
+
throw :bastard, :took_the_bait
|
28
|
+
end
|
29
|
+
end
|
30
|
+
if value.kind_of?(Hash)
|
31
|
+
deobfuscate!(value, object_name ? "#{object_name}[#{key}]" : key)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class BotAway::Spinner
|
2
|
+
def initialize(ip, key, secret)
|
3
|
+
raise "Shouldn't have a nil ip" unless ip
|
4
|
+
raise "Shouldn't have a nil secret" unless secret
|
5
|
+
secret = File.join(#Time.now.to_i.to_s,
|
6
|
+
ip,
|
7
|
+
key.to_s,
|
8
|
+
secret)
|
9
|
+
|
10
|
+
#puts secret
|
11
|
+
@spinner = Digest::MD5.hexdigest(secret)
|
12
|
+
#puts @spinner
|
13
|
+
end
|
14
|
+
|
15
|
+
def spinner
|
16
|
+
@spinner
|
17
|
+
end
|
18
|
+
|
19
|
+
def encode(real_field_name)
|
20
|
+
Digest::MD5.hexdigest(File.join(real_field_name.to_s, spinner))
|
21
|
+
end
|
22
|
+
end
|
data/script/console
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# File: script/console
|
3
|
+
irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
|
4
|
+
|
5
|
+
libs = " -r irb/completion"
|
6
|
+
# Perhaps use a console_lib to store any extra methods I may want available in the cosole
|
7
|
+
# libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
|
8
|
+
libs << " -r #{File.dirname(__FILE__) + '/../lib/bot-away.rb'}"
|
9
|
+
puts "Loading bot-away gem"
|
10
|
+
exec "#{irb} #{libs} --simple-prompt"
|
data/script/destroy
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rubigen'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rubigen'
|
9
|
+
end
|
10
|
+
require 'rubigen/scripts/destroy'
|
11
|
+
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
13
|
+
RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
|
14
|
+
RubiGen::Scripts::Destroy.new.run(ARGV)
|
data/script/generate
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rubigen'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rubigen'
|
9
|
+
end
|
10
|
+
require 'rubigen/scripts/generate'
|
11
|
+
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
13
|
+
RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
|
14
|
+
RubiGen::Scripts::Generate.new.run(ARGV)
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TestController do
|
4
|
+
include ActionController::TestProcess
|
5
|
+
|
6
|
+
def prepare!(action = 'index', method = 'get')
|
7
|
+
@controller.request = @request
|
8
|
+
@controller.params = {}
|
9
|
+
@controller.send(:initialize_current_url)
|
10
|
+
send(method, action)
|
11
|
+
if @response.template.instance_variable_get("@exception")
|
12
|
+
raise @response.template.instance_variable_get("@exception")
|
13
|
+
end
|
14
|
+
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
|
+
|
25
|
+
after :each do
|
26
|
+
# effectively disables forgery protection.
|
27
|
+
TestController.request_forgery_protection_token = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
context "with a model" do
|
31
|
+
context "with forgery protection" do
|
32
|
+
before :each do
|
33
|
+
(class << @controller; self; end).send(:protect_from_forgery)
|
34
|
+
prepare!('model_form')
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should work?" do
|
38
|
+
puts @response.body
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "without forgery protection" do
|
43
|
+
before :each do
|
44
|
+
prepare!('model_form')
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should work?" do
|
48
|
+
puts @response.body
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context "with forgery protection" do
|
54
|
+
before :each do
|
55
|
+
(class << @controller; self; end).send(:protect_from_forgery)
|
56
|
+
prepare!
|
57
|
+
end
|
58
|
+
#"object_name_method_name" name="object_name[method_name]" size="30" type="text" value="" /></div>
|
59
|
+
#<input id="e21372563297c728093bf74c3cb6b96c" name="a0844d45bf150668ff1d86a6eb491969" size="30" type="text" value="method_value" />
|
60
|
+
|
61
|
+
it "processes valid obfuscated form post" do
|
62
|
+
form = { 'authenticity_token' => '1234',
|
63
|
+
'object_name' => { 'method_name' => '' },
|
64
|
+
'842d8d1c80014ce9f3d974614338605c' => 'some_value'
|
65
|
+
}
|
66
|
+
post 'proc_form', form
|
67
|
+
puts @response.body
|
68
|
+
@response.template.controller.params[:object_name].should == { 'method_name' => 'some_value' }
|
69
|
+
end
|
70
|
+
|
71
|
+
it "drops invalid obfuscated form post" do
|
72
|
+
form = { 'authenticity_token' => '1234',
|
73
|
+
'object_name' => { 'method_name' => 'test' },
|
74
|
+
'842d8d1c80014ce9f3d974614338605c' => 'some_value'
|
75
|
+
}
|
76
|
+
post 'proc_form', form
|
77
|
+
puts @response.body
|
78
|
+
@response.template.controller.params.should == { 'suspected_bot' => true }
|
79
|
+
end
|
80
|
+
|
81
|
+
it "does not drop valid authentication request" do
|
82
|
+
#@request.session[:_csrf_token] = 'yPgTAsngzpBO8k1v83RGH26sTrQYD50Ou2oiMT4r/iw='
|
83
|
+
form = { 'authenticity_token' => 'yPgTAsngzpBO8k1v83RGH26sTrQYD50Ou2oiMT4r/iw=',
|
84
|
+
'user_session' => {
|
85
|
+
'login' => '',
|
86
|
+
'password' => '',
|
87
|
+
'remember_me' => ''
|
88
|
+
},
|
89
|
+
'commit' => 'Log In',
|
90
|
+
'89dce8f562b119a2f88da6d29f535a0d' => 'admin',
|
91
|
+
'4b9bab79bc1b1cd5229041c357750e0c' => 'pwpwpw',
|
92
|
+
'256307a36284445cc84014dae651f2ed' => '1'
|
93
|
+
}
|
94
|
+
@request.remote_addr = '127.0.0.1'
|
95
|
+
post 'proc_form', form
|
96
|
+
puts @response.template.controller.params.inspect
|
97
|
+
@response.template.controller.params.should == { 'action' => 'proc_form', 'controller' => 'test',
|
98
|
+
'authenticity_token' => 'yPgTAsngzpBO8k1v83RGH26sTrQYD50Ou2oiMT4r/iw=',
|
99
|
+
'user_session' => {
|
100
|
+
'login' => 'admin',
|
101
|
+
'password' => 'pwpwpw',
|
102
|
+
'remember_me' => '1'
|
103
|
+
},
|
104
|
+
'commit' => 'Log In'
|
105
|
+
}
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context "without forgery protection" do
|
110
|
+
before :each do
|
111
|
+
prepare!
|
112
|
+
end
|
113
|
+
|
114
|
+
it "processes non-obfuscated form post" do
|
115
|
+
form = { #'authenticity_token' => '1234',
|
116
|
+
'object_name' => { 'method_name' => 'test' }
|
117
|
+
}
|
118
|
+
post 'proc_form', form
|
119
|
+
puts @response.body
|
120
|
+
@response.template.controller.params.should_not == { 'suspected_bot' => true }
|
121
|
+
@response.template.controller.params[:object_name].should == { 'method_name' => 'test' }
|
122
|
+
end
|
123
|
+
|
124
|
+
it "produces non-obfuscated form elements" do
|
125
|
+
@response.body.should_not match(/<\/div><input/)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
def template
|
4
|
+
return @response if @response
|
5
|
+
@response = TestController.call(Rack::MockRequest.env_for('/').merge({'REQUEST_URI' => '/',
|
6
|
+
'REMOTE_ADDR' => '127.0.0.1'}))
|
7
|
+
@response.template.controller.request_forgery_protection_token = :authenticity_token
|
8
|
+
@response.template.controller.session[:_csrf_token] = '1234'
|
9
|
+
@response.template
|
10
|
+
end
|
11
|
+
|
12
|
+
def mock_object
|
13
|
+
@mock_object ||= MockObject.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def default_instance_tag
|
17
|
+
ActionView::Helpers::InstanceTag.new("object_name", "method_name", template, mock_object)
|
18
|
+
end
|
19
|
+
|
20
|
+
describe ActionView::Helpers::InstanceTag do
|
21
|
+
subject { default_instance_tag }
|
22
|
+
|
23
|
+
context "with a valid text area tag" do
|
24
|
+
subject do
|
25
|
+
dump { default_instance_tag.to_text_area_tag }
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should produce blank honeypot value" do
|
29
|
+
subject.should_not =~ /name="object_name\[method_name\]"[^>]+>method_value<\/textarea>/
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context "with a valid input type=text tag" do
|
34
|
+
before(:each) { @tag_options = ["input", {:type => 'text', 'name' => 'object_name[method_name]', 'id' => 'object_name_method_name', 'value' => 'method_value'}] }
|
35
|
+
#subject { dump { default_instance_tag.tag(*@tag_options) } }
|
36
|
+
|
37
|
+
it "should turn off autocomplete for honeypots" do
|
38
|
+
subject.honeypot_tag(*@tag_options).should =~ /autocomplete="off"/
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should obfuscate tag name" do
|
42
|
+
subject.obfuscated_tag(*@tag_options).should =~ /name="a0844d45bf150668ff1d86a6eb491969"/
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should obfuscate tag id" do
|
46
|
+
subject.obfuscated_tag(*@tag_options).should =~ /id="e21372563297c728093bf74c3cb6b96c"/
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should not obfuscate tag value" do
|
50
|
+
subject.obfuscated_tag(*@tag_options).should_not =~ /value="5a6a50d5fd0b5c8b1190d87eb0057e47"/
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should include unobfuscated tag value" do
|
54
|
+
subject.obfuscated_tag(*@tag_options).should =~ /value="method_value"/
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should create honeypot name" do
|
58
|
+
subject.honeypot_tag(*@tag_options).should =~ /name="object_name\[method_name\]"/
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should create honeypot id" do
|
62
|
+
subject.honeypot_tag(*@tag_options).should =~ /id="object_name_method_name"/
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should create empty honeypot tag value" do
|
66
|
+
subject.honeypot_tag(*@tag_options).should =~ /value=""/
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
###
|
2
|
+
# The original implementation of BotAway extended ActionView::Helpers::FormBuilder, and these tests were written
|
3
|
+
# for it. This approach has since been abandoned in favor of a direct override of ActionView::Helpers::InstanceTag for
|
4
|
+
# reasons of efficiency. The FormBuilder tests have been kept around simply for an extra layer of functional testing.
|
5
|
+
###
|
6
|
+
|
7
|
+
require 'spec_helper'
|
8
|
+
|
9
|
+
class MockObject; attr_accessor :method_name; def initialize; @method_name = 'method_value'; end; end
|
10
|
+
|
11
|
+
describe ActionView::Helpers::FormBuilder do
|
12
|
+
subject { builder }
|
13
|
+
|
14
|
+
it "should not create honeypots with default values" do
|
15
|
+
builder.text_field(:method_name).should match(/name="object_name\[method_name\]"[^>]*?value=""/)
|
16
|
+
end
|
17
|
+
|
18
|
+
# select(method, choices, options = {}, html_options = {})
|
19
|
+
obfuscates(:select) { builder.select(:method_name, {1 => :a, 2 => :b }) }
|
20
|
+
|
21
|
+
#collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
|
22
|
+
obfuscates(:collection_select) { builder.collection_select method_name, [MockObject.new], :method_name, :method_name }
|
23
|
+
|
24
|
+
#grouped_collection_select(method, collection, group_method, group_label_method, option_key_method,
|
25
|
+
# option_value_method, options = {}, html_options = {})
|
26
|
+
obfuscates(:grouped_collection_select) do
|
27
|
+
builder.grouped_collection_select method_name, [MockObject.new], :method_name, :method_name, :to_s, :to_s
|
28
|
+
end
|
29
|
+
|
30
|
+
#time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
|
31
|
+
obfuscates(:time_zone_select) do
|
32
|
+
builder.time_zone_select method_name
|
33
|
+
end
|
34
|
+
|
35
|
+
%w(hidden_field text_field text_area file_field password_field check_box).each do |field|
|
36
|
+
obfuscates(field) { builder.send(field, method_name) }
|
37
|
+
end
|
38
|
+
|
39
|
+
obfuscates(:radio_button, '53640013be550817d040597218884288') { builder.radio_button method_name, :value }
|
40
|
+
|
41
|
+
context "#label" do
|
42
|
+
subject { dump { builder.label(method_name) } }
|
43
|
+
|
44
|
+
it "links labels to their obfuscated elements" do
|
45
|
+
subject.should match(/for=\"e21372563297c728093bf74c3cb6b96c\"/)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
describe BotAway::ParamParser do
|
2
|
+
def params(honeypots)
|
3
|
+
@params = { 'authenticity_token' => '1234',
|
4
|
+
'bdf3e1964ac3a82c5f1c7385ed0100e4' => 'colin',
|
5
|
+
'ed8fc4fe67d66bc7aeaf0b4817bd1311' => [1, 2]
|
6
|
+
}.merge(honeypots).with_indifferent_access
|
7
|
+
end
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
# Root level is encoded with 208.77.188.166/test/1234
|
11
|
+
# which resolves to a spinner digest of 86ba3fd99e851587a849ad9ed9817f9b
|
12
|
+
@ip = '208.77.188.166'
|
13
|
+
@params = params('test' => { 'name' => '', 'posts' => [] })
|
14
|
+
end
|
15
|
+
|
16
|
+
subject { r = BotAway::ParamParser.new(@ip, @params); puts r.params.to_yaml; r }
|
17
|
+
|
18
|
+
context "with blank honeypots" do
|
19
|
+
it "drops obfuscated params" do
|
20
|
+
subject.params.keys.should_not include('bdf3e1964ac3a82c5f1c7385ed0100e4')
|
21
|
+
end
|
22
|
+
|
23
|
+
it "drops obfuscated subparams" do
|
24
|
+
subject.params.keys.should_not include('ed8fc4fe67d66bc7aeaf0b4817bd1311')
|
25
|
+
end
|
26
|
+
|
27
|
+
it "replaces honeypots" do
|
28
|
+
subject.params[:test].should_not be_blank
|
29
|
+
end
|
30
|
+
|
31
|
+
it "replaces subhoneypots" do
|
32
|
+
subject.params.keys.should include('test')
|
33
|
+
subject.params[:test][:name].should == 'colin'
|
34
|
+
subject.params[:test][:posts].should == [1,2]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context "with a filled honeypot" do
|
39
|
+
before(:each) { @params = params({'test' => {'name' => 'colin', 'posts' => []}}) }
|
40
|
+
subject { r = BotAway::ParamParser.new(@ip, @params); puts r.params.to_yaml; r }
|
41
|
+
|
42
|
+
it "drops all parameters" do
|
43
|
+
subject.params.should == { "suspected_bot" => true }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context "with a filled sub-honeypot" do
|
48
|
+
before(:each) { @params = params({'test' => {'name' => '', 'posts' => [1, 2]}}) }
|
49
|
+
subject { r = BotAway::ParamParser.new(@ip, @params); puts r.params.to_yaml; r }
|
50
|
+
|
51
|
+
it "drops all parameters" do
|
52
|
+
subject.params.should == { "suspected_bot" => true }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'action_controller'
|
3
|
+
require 'action_view'
|
4
|
+
|
5
|
+
ActionController::Routing::Routes.load!
|
6
|
+
ActionController::Base.session = { :key => "_myapp_session", :secret => "12345"*6 }
|
7
|
+
|
8
|
+
require File.join(File.dirname(__FILE__), '../lib/bot-away')
|
9
|
+
|
10
|
+
Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each do |fi|
|
11
|
+
require fi
|
12
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Post
|
2
|
+
attr_reader :subject, :body, :subscribers
|
3
|
+
end
|
4
|
+
|
5
|
+
class TestController < ActionController::Base
|
6
|
+
view_paths << File.expand_path(File.join(File.dirname(__FILE__), "../views"))
|
7
|
+
|
8
|
+
def index
|
9
|
+
end
|
10
|
+
|
11
|
+
def model_form
|
12
|
+
@post = Post.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def proc_form
|
16
|
+
render :text => params.to_yaml
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class HoneypotMatcher
|
2
|
+
def initialize(object_name, method_name)
|
3
|
+
@object_name, @method_name = object_name, method_name
|
4
|
+
end
|
5
|
+
|
6
|
+
def matches?(target)
|
7
|
+
target = target.call if target.kind_of?(Proc)
|
8
|
+
@target = target
|
9
|
+
@rx = /name="#{Regexp::escape @object_name}\[#{Regexp::escape @method_name}/m
|
10
|
+
@target[@rx]
|
11
|
+
end
|
12
|
+
|
13
|
+
def failure_message
|
14
|
+
"expected #{@target.inspect}\n to match #{@rx.to_s}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def negative_failure_message
|
18
|
+
"expected #{@target.inspect}\n to not match #{@rx.to_s}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def include_honeypot(object_name, method_name)
|
23
|
+
HoneypotMatcher.new(object_name, method_name)
|
24
|
+
end
|
25
|
+
|
26
|
+
alias contain_honeypot include_honeypot
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module ObfuscationHelper
|
2
|
+
def includes_honeypot(object_name, method_name)
|
3
|
+
it "includes a honeypot called #{object_name}[#{method_name}]" do
|
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
|
12
|
+
end
|
13
|
+
|
14
|
+
def dump
|
15
|
+
returning(yield) { |x| puts x }
|
16
|
+
end
|
17
|
+
|
18
|
+
def builder
|
19
|
+
return @builder if @builder
|
20
|
+
response = TestController.call(Rack::MockRequest.env_for('/').merge({'REQUEST_URI' => '/',
|
21
|
+
'REMOTE_ADDR' => '127.0.0.1'}))
|
22
|
+
response.template.controller.request_forgery_protection_token = :authenticity_token
|
23
|
+
response.template.controller.session[:_csrf_token] = '1234'
|
24
|
+
@builder = ActionView::Helpers::FormBuilder.new(:object_name, MockObject.new, response.template, {}, proc {})
|
25
|
+
end
|
26
|
+
|
27
|
+
def obfuscates(method, obfuscated_id = self.obfuscated_id, obfuscated_name = self.obfuscated_name)
|
28
|
+
value = yield
|
29
|
+
context "##{method}" do
|
30
|
+
subject { proc { dump { value } } }
|
31
|
+
|
32
|
+
includes_honeypot(object_name, method_name)
|
33
|
+
is_obfuscated_as(obfuscated_id, obfuscated_name)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def obfuscated_id
|
38
|
+
"e21372563297c728093bf74c3cb6b96c"
|
39
|
+
end
|
40
|
+
|
41
|
+
def obfuscated_name
|
42
|
+
"a0844d45bf150668ff1d86a6eb491969"
|
43
|
+
end
|
44
|
+
|
45
|
+
def object_name
|
46
|
+
"object_name"
|
47
|
+
end
|
48
|
+
|
49
|
+
def method_name
|
50
|
+
"method_name"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
Spec::Runner.configure do |config|
|
55
|
+
config.extend ObfuscationHelper
|
56
|
+
config.include ObfuscationHelper
|
57
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class ObfuscationMatcher
|
2
|
+
def initialize(id, name)
|
3
|
+
@id, @name = id, name
|
4
|
+
end
|
5
|
+
|
6
|
+
def matches?(target)
|
7
|
+
target = target.call if target.kind_of?(Proc)
|
8
|
+
@target = target
|
9
|
+
match(:id) && match(:name)
|
10
|
+
end
|
11
|
+
|
12
|
+
def match(which)
|
13
|
+
@rx = /#{which}=['"]#{Regexp::escape instance_variable_get("@#{which}")}/
|
14
|
+
@target[@rx]
|
15
|
+
end
|
16
|
+
|
17
|
+
def failure_message
|
18
|
+
"expected #{@target.inspect}\n to match #{@rx.inspect}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def negative_failure_message
|
22
|
+
"expected #{@target.inspect}\n to not match #{@rx.inspect}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def be_obfuscated_as(id, name)
|
27
|
+
ObfuscationMatcher.new(id, name)
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bot-away
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
version: 1.0.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Colin MacKenzie IV
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-04-01 00:00:00 -04:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: actionpack
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 2
|
29
|
+
- 3
|
30
|
+
- 5
|
31
|
+
version: 2.3.5
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: sc-core-ext
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 1
|
43
|
+
- 1
|
44
|
+
- 1
|
45
|
+
version: 1.1.1
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id002
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: rubyforge
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
segments:
|
56
|
+
- 2
|
57
|
+
- 0
|
58
|
+
- 3
|
59
|
+
version: 2.0.3
|
60
|
+
type: :development
|
61
|
+
version_requirements: *id003
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: gemcutter
|
64
|
+
prerelease: false
|
65
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
segments:
|
70
|
+
- 0
|
71
|
+
- 5
|
72
|
+
- 0
|
73
|
+
version: 0.5.0
|
74
|
+
type: :development
|
75
|
+
version_requirements: *id004
|
76
|
+
- !ruby/object:Gem::Dependency
|
77
|
+
name: hoe
|
78
|
+
prerelease: false
|
79
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
segments:
|
84
|
+
- 2
|
85
|
+
- 5
|
86
|
+
- 0
|
87
|
+
version: 2.5.0
|
88
|
+
type: :development
|
89
|
+
version_requirements: *id005
|
90
|
+
description: |-
|
91
|
+
Unobtrusively detects form submissions made by spambots, and silently drops those submissions. The key word here is
|
92
|
+
"unobtrusive" -- this is NOT a CAPTCHA. This is transparent, modular implementation of the bot-catching techniques
|
93
|
+
discussed by Ned Batchelder at http://nedbatchelder.com/text/stopbots.html
|
94
|
+
|
95
|
+
If a submission is detected, the params hash is cleared, so the data can't be used. Since this includes the authenticity
|
96
|
+
token, Rails should barf due to an invalid or missing authenticity token. Congrats, spam blocked.
|
97
|
+
|
98
|
+
The specifics of the techniques employed for filtering spambots are discussed Ned's site at the above location; however,
|
99
|
+
here's a brief run-down of what's going on:
|
100
|
+
|
101
|
+
* Your code stays the same. After the bot-away gem has been activated, all Rails-generated forms on your site
|
102
|
+
will automatically be transformed into bot-resistent forms.
|
103
|
+
* All of the form elements that you create (for instance, a "comment" model with a "body" field) are turned into
|
104
|
+
dummy elements, or honeypots, and are made invisible to the end user. This is done using div elements and inline CSS
|
105
|
+
stylesheets. There are several ways an element can be hidden, and these approaches are chosen at random to help
|
106
|
+
minimize predictability. In the rare event that a real user actually can see the element, it has a label next to it
|
107
|
+
along the lines of "Leave this blank" -- though the exact message is randomized to help prevent detection.
|
108
|
+
* All of the form elements are mirrored by hashes. The hashes are generated using the session's authenticity token,
|
109
|
+
so they can't be predicted.
|
110
|
+
* When data is submitted, bot-away steps in and 1.) validates that no honeypots have been filled in; and 2)
|
111
|
+
converts the hashed elements back into the field names that you are expecting (replacing the honeypot fields).
|
112
|
+
* If a honeypot has been filled in, or a hashed element is missing where it was expected, then the request is
|
113
|
+
considered to be either spam, or tampered with; and the entire params hash is emptied. Since this happens at the
|
114
|
+
lowest level, the most likely result is that Rails will complain that the user's authenticity token is invalid. If
|
115
|
+
that does not happen, then your code will be passed a params hash containing only a "suspected_bot" key, and an error
|
116
|
+
will result. Either way, the spambot has been foiled!
|
117
|
+
email:
|
118
|
+
- sinisterchipmunk@gmail.com
|
119
|
+
executables: []
|
120
|
+
|
121
|
+
extensions: []
|
122
|
+
|
123
|
+
extra_rdoc_files:
|
124
|
+
- History.txt
|
125
|
+
- Manifest.txt
|
126
|
+
files:
|
127
|
+
- History.txt
|
128
|
+
- Manifest.txt
|
129
|
+
- README.rdoc
|
130
|
+
- Rakefile
|
131
|
+
- lib/bot-away.rb
|
132
|
+
- lib/bot-away/action_controller/request.rb
|
133
|
+
- lib/bot-away/action_view/helpers/instance_tag.rb
|
134
|
+
- lib/bot-away/param_parser.rb
|
135
|
+
- lib/bot-away/spinner.rb
|
136
|
+
- script/console
|
137
|
+
- script/destroy
|
138
|
+
- script/generate
|
139
|
+
- spec/controllers/test_controller_spec.rb
|
140
|
+
- spec/lib/action_view/helpers/instance_tag_spec.rb
|
141
|
+
- spec/lib/builder_spec.rb
|
142
|
+
- spec/lib/param_parser_spec.rb
|
143
|
+
- spec/spec_helper.rb
|
144
|
+
- spec/support/controllers/test_controller.rb
|
145
|
+
- spec/support/honeypot_matcher.rb
|
146
|
+
- spec/support/obfuscation_helper.rb
|
147
|
+
- spec/support/obfuscation_matcher.rb
|
148
|
+
- spec/support/views/test/index.html.erb
|
149
|
+
- spec/support/views/test/model_form.html.erb
|
150
|
+
has_rdoc: true
|
151
|
+
homepage: http://github.com/sinisterchipmunk/bot-away
|
152
|
+
licenses: []
|
153
|
+
|
154
|
+
post_install_message:
|
155
|
+
rdoc_options:
|
156
|
+
- --main
|
157
|
+
- README.rdoc
|
158
|
+
require_paths:
|
159
|
+
- lib
|
160
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
161
|
+
requirements:
|
162
|
+
- - ">="
|
163
|
+
- !ruby/object:Gem::Version
|
164
|
+
segments:
|
165
|
+
- 0
|
166
|
+
version: "0"
|
167
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
168
|
+
requirements:
|
169
|
+
- - ">="
|
170
|
+
- !ruby/object:Gem::Version
|
171
|
+
segments:
|
172
|
+
- 0
|
173
|
+
version: "0"
|
174
|
+
requirements: []
|
175
|
+
|
176
|
+
rubyforge_project: bot-away
|
177
|
+
rubygems_version: 1.3.6
|
178
|
+
signing_key:
|
179
|
+
specification_version: 3
|
180
|
+
summary: Unobtrusively detects form submissions made by spambots, and silently drops those submissions
|
181
|
+
test_files: []
|
182
|
+
|