bot-away 1.0.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.
- 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
|
+
|