invisible_captcha 0.12.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +5 -18
- data/Appraisals +1 -5
- data/CHANGELOG.md +27 -0
- data/Gemfile +2 -0
- data/LICENSE +1 -1
- data/README.md +116 -11
- data/Rakefile +2 -0
- data/gemfiles/rails_6.0.gemfile +1 -1
- data/invisible_captcha.gemspec +1 -4
- data/lib/invisible_captcha.rb +2 -0
- data/lib/invisible_captcha/controller_ext.rb +25 -18
- data/lib/invisible_captcha/form_helpers.rb +3 -1
- data/lib/invisible_captcha/railtie.rb +2 -0
- data/lib/invisible_captcha/version.rb +3 -1
- data/lib/invisible_captcha/view_helpers.rb +9 -1
- data/spec/controllers_spec.rb +56 -3
- data/spec/dummy/app/assets/config/manifest.js +2 -0
- data/spec/dummy/config/environments/test.rb +1 -1
- data/spec/invisible_captcha_spec.rb +2 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/view_helpers_spec.rb +12 -0
- metadata +6 -33
- data/gemfiles/rails_3.2.gemfile +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0d931568dd6707074ae2b5f48f6a5552283d482a4d87f27b1c72852dfce3d9cb
|
4
|
+
data.tar.gz: 4e78f33d0a7c4be1a774de1e98e55d7fd2f7ff84e1a407d66a565c6e99779fa9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3fbe8b6755bbc31fb26fbf9d71f01b7c9dbe919feaaaf12f78f86f7e53e93bb9f04f52c83469ea59840714908d834abf84625a542e7f55de2b7f0cd1d877986c
|
7
|
+
data.tar.gz: 380701e4ddf138445faafb293ed94fe368d58eb7d995d44b3f871362550d5c2bb27d537d00c04cdf89501bc951ab227d3fe7c721389ba1f163327f4d8093ced4
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -1,20 +1,17 @@
|
|
1
1
|
language: ruby
|
2
2
|
cache: bundler
|
3
|
-
sudo: false
|
4
3
|
rvm:
|
5
4
|
- ruby-head
|
6
|
-
- 2.6.
|
7
|
-
- 2.5.
|
8
|
-
- 2.4.
|
5
|
+
- 2.6.5
|
6
|
+
- 2.5.7
|
7
|
+
- 2.4.9
|
9
8
|
- 2.3.8
|
10
|
-
- 2.2.10
|
11
9
|
gemfile:
|
12
10
|
- gemfiles/rails_6.0.gemfile
|
13
11
|
- gemfiles/rails_5.2.gemfile
|
14
12
|
- gemfiles/rails_5.1.gemfile
|
15
13
|
- gemfiles/rails_5.0.gemfile
|
16
14
|
- gemfiles/rails_4.2.gemfile
|
17
|
-
- gemfiles/rails_3.2.gemfile
|
18
15
|
before_install:
|
19
16
|
# Rails 4.x requires Bundler version < 2.0.
|
20
17
|
- "find /home/travis/.rvm/rubies -wholename '*default/bundler-*.gemspec' -delete"
|
@@ -22,19 +19,9 @@ before_install:
|
|
22
19
|
- rvm @global do yes | gem install bundler -v '< 2'
|
23
20
|
matrix:
|
24
21
|
exclude:
|
25
|
-
- rvm: 2.4.
|
22
|
+
- rvm: 2.4.9
|
26
23
|
gemfile: gemfiles/rails_6.0.gemfile
|
27
24
|
- rvm: 2.3.8
|
28
25
|
gemfile: gemfiles/rails_6.0.gemfile
|
29
|
-
- rvm: 2.2.10
|
30
|
-
gemfile: gemfiles/rails_6.0.gemfile
|
31
|
-
- rvm: ruby-head
|
32
|
-
gemfile: gemfiles/rails_3.2.gemfile
|
33
|
-
- rvm: 2.6.2
|
34
|
-
gemfile: gemfiles/rails_3.2.gemfile
|
35
|
-
- rvm: 2.5.5
|
36
|
-
gemfile: gemfiles/rails_3.2.gemfile
|
37
|
-
- rvm: 2.4.5
|
38
|
-
gemfile: gemfiles/rails_3.2.gemfile
|
39
26
|
allow_failures:
|
40
|
-
- rvm: ruby-head
|
27
|
+
- rvm: ruby-head
|
data/Appraisals
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
appraise "rails-6.0" do
|
2
|
-
gem "rails", "6.0.0
|
2
|
+
gem "rails", "~> 6.0.0"
|
3
3
|
end
|
4
4
|
|
5
5
|
appraise "rails-5.2" do
|
@@ -17,7 +17,3 @@ end
|
|
17
17
|
appraise "rails-4.2" do
|
18
18
|
gem "rails", "~> 4.2.0"
|
19
19
|
end
|
20
|
-
|
21
|
-
appraise "rails-3.2" do
|
22
|
-
gem "rails", "~> 3.2.0"
|
23
|
-
end
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,28 @@
|
|
2
2
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
4
4
|
|
5
|
+
## [1.1.0]
|
6
|
+
|
7
|
+
- New option `prepend: true` for the controller macro (#77)
|
8
|
+
|
9
|
+
## [1.0.1]
|
10
|
+
|
11
|
+
- Fix naming issue with Ruby 2.7 (#65)
|
12
|
+
|
13
|
+
## [1.0.0]
|
14
|
+
|
15
|
+
- Remove Ruby 2.2 and Rails 3.2 support
|
16
|
+
- Add Instrumentation event (#62)
|
17
|
+
|
18
|
+
## [0.13.0]
|
19
|
+
|
20
|
+
- Add support for the Content Security Policy nonce (#61)
|
21
|
+
- Freeze all strings (#60)
|
22
|
+
|
23
|
+
## [0.12.2]
|
24
|
+
|
25
|
+
- Allow new timestamp to be set during `on_timestamp_spam` callback (#53)
|
26
|
+
|
5
27
|
## [0.12.1]
|
6
28
|
|
7
29
|
- Clear timestamp stored in `session[:invisible_captcha_timestamp]` (#50)
|
@@ -97,6 +119,11 @@ All notable changes to this project will be documented in this file.
|
|
97
119
|
|
98
120
|
- First version of controller filters
|
99
121
|
|
122
|
+
[1.1.0]: https://github.com/markets/invisible_captcha/compare/v1.0.1...v1.1.0
|
123
|
+
[1.0.1]: https://github.com/markets/invisible_captcha/compare/v1.0.0...v1.0.1
|
124
|
+
[1.0.0]: https://github.com/markets/invisible_captcha/compare/v0.13.0...v1.0.0
|
125
|
+
[0.13.0]: https://github.com/markets/invisible_captcha/compare/v0.12.2...v0.13.0
|
126
|
+
[0.12.2]: https://github.com/markets/invisible_captcha/compare/v0.12.1...v0.12.2
|
100
127
|
[0.12.1]: https://github.com/markets/invisible_captcha/compare/v0.12.0...v0.12.1
|
101
128
|
[0.12.0]: https://github.com/markets/invisible_captcha/compare/v0.11.0...v0.12.0
|
102
129
|
[0.11.0]: https://github.com/markets/invisible_captcha/compare/v0.10.0...v0.11.0
|
data/Gemfile
CHANGED
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -7,19 +7,19 @@
|
|
7
7
|
|
8
8
|
Invisible Captcha provides different techniques to protect your application against spambots.
|
9
9
|
|
10
|
-
The main protection is a solution based on the `honeypot` principle, which provides a better user experience
|
10
|
+
The main protection is a solution based on the `honeypot` principle, which provides a better user experience since there are no extra steps for real users, only for the bots.
|
11
11
|
|
12
12
|
Essentially, the strategy consists on adding an input field :honey_pot: into the form that:
|
13
13
|
|
14
14
|
- shouldn't be visible by the real users
|
15
15
|
- should be left empty by the real users
|
16
|
-
- will most be filled by spam bots
|
16
|
+
- will most likely be filled by spam bots
|
17
17
|
|
18
18
|
It also comes with a time-sensitive :hourglass: form submission.
|
19
19
|
|
20
20
|
## Installation
|
21
21
|
|
22
|
-
Invisible Captcha is tested against Rails `>=
|
22
|
+
Invisible Captcha is tested against Rails `>= 4.2` and Ruby `>= 2.3`.
|
23
23
|
|
24
24
|
Add this line to your Gemfile and then execute `bundle install`:
|
25
25
|
|
@@ -47,7 +47,7 @@ class TopicsController < ApplicationController
|
|
47
47
|
end
|
48
48
|
```
|
49
49
|
|
50
|
-
This method will act as a `before_action` that triggers when spam is detected (honeypot field has some value). By default it responds with no content (only headers: `head(200)`). This is a good default, since the bot will surely read the response code and will think that it has achieved to submit the form properly. But, anyway, you
|
50
|
+
This method will act as a `before_action` that triggers when spam is detected (honeypot field has some value). By default, it responds with no content (only headers: `head(200)`). This is a good default, since the bot will surely read the response code and will think that it has achieved to submit the form properly. But, anyway, you can define your own callback by passing a method to the `on_spam` option:
|
51
51
|
|
52
52
|
```ruby
|
53
53
|
class TopicsController < ApplicationController
|
@@ -61,7 +61,7 @@ class TopicsController < ApplicationController
|
|
61
61
|
end
|
62
62
|
```
|
63
63
|
|
64
|
-
Note that is not mandatory to specify a `honeypot` attribute (
|
64
|
+
Note that it is not mandatory to specify a `honeypot` attribute (neither in the view nor in the controller). In this case, the engine will take a random field from `InvisibleCaptcha.honeypots`. So, if you're integrating it following this path, in your form:
|
65
65
|
|
66
66
|
```erb
|
67
67
|
<%= form_tag(new_contact_path) do |f| %>
|
@@ -69,12 +69,32 @@ Note that is not mandatory to specify a `honeypot` attribute (nor in the view, n
|
|
69
69
|
<% end %>
|
70
70
|
```
|
71
71
|
|
72
|
-
In
|
72
|
+
In your controller:
|
73
73
|
|
74
74
|
```
|
75
75
|
invisible_captcha only: [:new_contact]
|
76
76
|
```
|
77
77
|
|
78
|
+
`invisible_captcha` sends all messages to `flash[:error]`. For messages to appear on your pages, add `<%= flash[:error] %>` to `app/views/layouts/application.html.erb` (somewhere near the top of your `<body>` element):
|
79
|
+
|
80
|
+
```erb
|
81
|
+
<!DOCTYPE html>
|
82
|
+
<html>
|
83
|
+
<head>
|
84
|
+
<title>Yet another Rails app</title>
|
85
|
+
<%= stylesheet_link_tag "application", media: "all" %>
|
86
|
+
<%= javascript_include_tag "application" %>
|
87
|
+
<%= csrf_meta_tags %>
|
88
|
+
</head>
|
89
|
+
<body>
|
90
|
+
<%= flash[:error] %>
|
91
|
+
<%= yield %>
|
92
|
+
</body>
|
93
|
+
</html>
|
94
|
+
```
|
95
|
+
|
96
|
+
You can place `<%= flash[:error] %>` next to `:alert` and `:notice` message types, if you have them in your `app/views/layouts/application.html.erb`.
|
97
|
+
|
78
98
|
## Options and customization
|
79
99
|
|
80
100
|
This section contains a description of all plugin options and customizations.
|
@@ -84,7 +104,7 @@ This section contains a description of all plugin options and customizations.
|
|
84
104
|
You can customize:
|
85
105
|
|
86
106
|
- `sentence_for_humans`: text for real users if input field was visible. By default, it uses I18n (see below).
|
87
|
-
- `honeypots`: collection of default honeypots. Used by the view helper, called with no args, to generate a random honeypot field name. By default, a random collection is already generated.
|
107
|
+
- `honeypots`: collection of default honeypots. Used by the view helper, called with no args, to generate a random honeypot field name. By default, a random collection is already generated. As the random collection is stored in memory, it will not work if are running multiple Rails instances behind a load balancer. See [Multiple Rails instances](#multiple-rails-instances).
|
88
108
|
- `visual_honeypots`: make honeypots visible, also useful to test/debug your implementation.
|
89
109
|
- `timestamp_threshold`: fastest time (in seconds) to expect a human to submit the form (see [original article by Yoav Aner](https://blog.gingerlime.com/2012/simple-detection-of-comment-spam-in-rails/) outlining the idea). By default, 4 seconds. **NOTE:** It's recommended to deactivate the autocomplete feature to avoid false positives (`autocomplete="off"`).
|
90
110
|
- `timestamp_enabled`: option to disable the time threshold check at application level. Could be useful, for example, on some testing scenarios. By default, true.
|
@@ -107,6 +127,20 @@ InvisibleCaptcha.setup do |config|
|
|
107
127
|
end
|
108
128
|
```
|
109
129
|
|
130
|
+
#### Multiple Rails instances
|
131
|
+
|
132
|
+
If you have multiple Rails instances running behind a load balancer, you have to share the same honeypots collection between the instances.
|
133
|
+
|
134
|
+
Either use a fixed collection or share them between the instances using `Rails.cache`:
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
InvisibleCaptcha.setup do |config|
|
138
|
+
config.honeypots = Rails.cache.fetch('invisible_captcha_honeypots') do
|
139
|
+
(1..20).map { InvisibleCaptcha.generate_random_honeypot }
|
140
|
+
end
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
110
144
|
### Controller method options:
|
111
145
|
|
112
146
|
The `invisible_captcha` method accepts some options:
|
@@ -119,6 +153,7 @@ The `invisible_captcha` method accepts some options:
|
|
119
153
|
- `timestamp_enabled`: enable/disable this technique at action level.
|
120
154
|
- `on_timestamp_spam`: custom callback to be called when form submitted too quickly. The default action redirects to `:back` printing a warning in `flash[:error]`.
|
121
155
|
- `timestamp_threshold`: custom threshold per controller/action. Overrides the global value for `InvisibleCaptcha.timestamp_threshold`.
|
156
|
+
- `prepend`: the spam detection will run in a `prepend_before_action` if `prepend: true`, otherwise will run in a `before_action`.
|
122
157
|
|
123
158
|
### View helpers options:
|
124
159
|
|
@@ -145,6 +180,73 @@ You can also pass html options to the input:
|
|
145
180
|
<%= invisible_captcha :subtitle, :topic, id: "your_id", class: "your_class" %>
|
146
181
|
```
|
147
182
|
|
183
|
+
### Spam detection notifications
|
184
|
+
|
185
|
+
In addition to the `on_spam` controller callback, you can use the [Active Support Instrumentation API](https://guides.rubyonrails.org/active_support_instrumentation.html) to set up a global event handler that fires whenever spam is detected. This is useful for advanced logging, background processing, etc.
|
186
|
+
|
187
|
+
To set up a global event handler, [subscribe](https://guides.rubyonrails.org/active_support_instrumentation.html#subscribing-to-an-event) to the `invisible_captcha.spam_detected` event in an initializer:
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
# config/initializers/invisible_captcha.rb
|
191
|
+
|
192
|
+
ActiveSupport::Notifications.subscribe('invisible_captcha.spam_detected') do |*args, data|
|
193
|
+
AwesomeLogger.warn(data[:message], data) # Log to an external logging service.
|
194
|
+
SpamRequest.create(data) # Record the blocked request in your database.
|
195
|
+
end
|
196
|
+
```
|
197
|
+
|
198
|
+
The `data` passed to the subscriber is hash containing information about the request that was detected as spam. For example:
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
{
|
202
|
+
message: "Invisible Captcha honeypot param 'subtitle' was present.",
|
203
|
+
remote_ip: '127.0.0.1',
|
204
|
+
user_agent: 'Chrome 77',
|
205
|
+
controller: 'users',
|
206
|
+
action: 'create',
|
207
|
+
url: 'http://example.com/users',
|
208
|
+
params: {
|
209
|
+
topic: { subtitle: 'foo' },
|
210
|
+
controller: 'users',
|
211
|
+
action: 'create'
|
212
|
+
}
|
213
|
+
}
|
214
|
+
```
|
215
|
+
|
216
|
+
_**Note:** `params` will be filtered according to your `Rails.application.config.filter_parameters` configuration, making them (probably) safe for logging. But always double-check that you're not inadvertently logging sensitive form data, like passwords and credit cards._
|
217
|
+
|
218
|
+
### Content Security Policy
|
219
|
+
|
220
|
+
If you're using a Content Security Policy (CSP) in your Rails app, you will need to generate a nonce on the server, and pass `nonce: true` attribute to the view helper. Uncomment the following lines in your `config/initializers/content_security_policy.rb` file:
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
# Be sure to restart your server when you modify this file.
|
224
|
+
|
225
|
+
# If you are using UJS then enable automatic nonce generation
|
226
|
+
Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
|
227
|
+
|
228
|
+
# Set the nonce only to specific directives
|
229
|
+
Rails.application.config.content_security_policy_nonce_directives = %w(style-src)
|
230
|
+
```
|
231
|
+
Note that if you are already generating nonce for scripts, you'd have to include `script-src` to `content_security_policy_nonce_directives` as well:
|
232
|
+
|
233
|
+
```ruby
|
234
|
+
Rails.application.config.content_security_policy_nonce_directives = %w(script-src style-src)
|
235
|
+
```
|
236
|
+
|
237
|
+
And in your view helper, you need to pass `nonce: true` to the `invisible_captcha` helper:
|
238
|
+
|
239
|
+
```erb
|
240
|
+
<%= invisible_captcha nonce: true %>
|
241
|
+
```
|
242
|
+
|
243
|
+
**WARNING:** Content Security Policy can break your site! If you already run a website with third-party scripts, styles, images, and fonts, it is highly recommended to enable CSP in report-only mode and observe warnings as they appear. Learn more at MDN:
|
244
|
+
|
245
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
|
246
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
|
247
|
+
|
248
|
+
Note that Content Security Policy only works on Rails 5.2 and up.
|
249
|
+
|
148
250
|
### I18n
|
149
251
|
|
150
252
|
`invisible_captcha` tries to use I18n when it's available by default. The keys it looks for are the following:
|
@@ -156,17 +258,20 @@ en:
|
|
156
258
|
timestamp_error_message: "Sorry, that was too quick! Please resubmit."
|
157
259
|
```
|
158
260
|
|
159
|
-
You can override the
|
261
|
+
You can override the English ones in your i18n config files as well as add new ones for other locales.
|
160
262
|
|
161
263
|
If you intend to use I18n with `invisible_captcha`, you _must not_ set `sentence_for_humans` or `timestamp_error_message` to strings in the setup phase.
|
162
264
|
|
163
265
|
## Testing your controllers
|
164
266
|
|
165
|
-
If you're encountering unexpected behaviour while testing controllers that use the `invisible_captcha` action filter, you may want to disable timestamp check for the test environment:
|
267
|
+
If you're encountering unexpected behaviour while testing controllers that use the `invisible_captcha` action filter, you may want to disable timestamp check for the test environment. Add the following snippet to the `config/initializers/invisible_captcha.rb` file:
|
166
268
|
|
167
269
|
```ruby
|
168
|
-
#
|
169
|
-
|
270
|
+
# Be sure to restart your server when you modify this file.
|
271
|
+
|
272
|
+
InvisibleCaptcha.setup do |config|
|
273
|
+
config.timestamp_enabled = !Rails.env.test?
|
274
|
+
end
|
170
275
|
```
|
171
276
|
|
172
277
|
Another option is to wait for the timestamp check to be valid:
|
data/Rakefile
CHANGED
data/gemfiles/rails_6.0.gemfile
CHANGED
data/invisible_captcha.gemspec
CHANGED
@@ -15,11 +15,8 @@ Gem::Specification.new do |spec|
|
|
15
15
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
16
16
|
spec.require_paths = ["lib"]
|
17
17
|
|
18
|
-
spec.add_dependency 'rails', '>=
|
18
|
+
spec.add_dependency 'rails', '>= 4.2'
|
19
19
|
|
20
20
|
spec.add_development_dependency 'rspec-rails', '~> 3.1'
|
21
21
|
spec.add_development_dependency 'appraisal'
|
22
|
-
spec.add_development_dependency 'test-unit', '~> 3.0'
|
23
|
-
spec.add_development_dependency 'byebug'
|
24
22
|
end
|
25
|
-
|
data/lib/invisible_captcha.rb
CHANGED
@@ -1,13 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module InvisibleCaptcha
|
2
4
|
module ControllerExt
|
3
5
|
module ClassMethods
|
4
6
|
def invisible_captcha(options = {})
|
5
|
-
if
|
6
|
-
|
7
|
+
if options.key?(:prepend)
|
8
|
+
prepend_before_action(options) do
|
7
9
|
detect_spam(options)
|
8
10
|
end
|
9
11
|
else
|
10
|
-
|
12
|
+
before_action(options) do
|
11
13
|
detect_spam(options)
|
12
14
|
end
|
13
15
|
end
|
@@ -22,8 +24,6 @@ module InvisibleCaptcha
|
|
22
24
|
elsif honeypot_spam?(options)
|
23
25
|
on_spam(options)
|
24
26
|
end
|
25
|
-
|
26
|
-
clear_session
|
27
27
|
end
|
28
28
|
|
29
29
|
def on_timestamp_spam(options = {})
|
@@ -55,28 +55,24 @@ module InvisibleCaptcha
|
|
55
55
|
|
56
56
|
return false unless enabled
|
57
57
|
|
58
|
-
|
58
|
+
@invisible_captcha_timestamp ||= session.delete(:invisible_captcha_timestamp)
|
59
59
|
|
60
60
|
# Consider as spam if timestamp not in session, cause that means the form was not fetched at all
|
61
|
-
unless
|
62
|
-
|
61
|
+
unless @invisible_captcha_timestamp
|
62
|
+
warn_spam("Invisible Captcha timestamp not found in session.")
|
63
63
|
return true
|
64
64
|
end
|
65
65
|
|
66
|
-
time_to_submit = Time.zone.now - DateTime.iso8601(
|
66
|
+
time_to_submit = Time.zone.now - DateTime.iso8601(@invisible_captcha_timestamp)
|
67
67
|
threshold = options[:timestamp_threshold] || InvisibleCaptcha.timestamp_threshold
|
68
68
|
|
69
69
|
# Consider as spam if form submitted too quickly
|
70
70
|
if time_to_submit < threshold
|
71
|
-
|
71
|
+
warn_spam("Invisible Captcha timestamp threshold not reached (took #{time_to_submit.to_i}s).")
|
72
72
|
return true
|
73
73
|
end
|
74
74
|
|
75
|
-
false
|
76
|
-
end
|
77
|
-
|
78
|
-
def clear_session
|
79
|
-
session.delete(:invisible_captcha_timestamp) if session[:invisible_captcha_timestamp]
|
75
|
+
return false
|
80
76
|
end
|
81
77
|
|
82
78
|
def honeypot_spam?(options = {})
|
@@ -88,7 +84,7 @@ module InvisibleCaptcha
|
|
88
84
|
# - honeypot: params[:subtitle]
|
89
85
|
# - honeypot with scope: params[:topic][:subtitle]
|
90
86
|
if params[honeypot].present? || (params[scope] && params[scope][honeypot].present?)
|
91
|
-
|
87
|
+
warn_spam("Invisible Captcha honeypot param '#{honeypot}' was present.")
|
92
88
|
return true
|
93
89
|
else
|
94
90
|
# No honeypot spam detected, remove honeypot from params to avoid UnpermittedParameters exceptions
|
@@ -98,7 +94,7 @@ module InvisibleCaptcha
|
|
98
94
|
else
|
99
95
|
InvisibleCaptcha.honeypots.each do |default_honeypot|
|
100
96
|
if params[default_honeypot].present?
|
101
|
-
|
97
|
+
warn_spam("Invisible Captcha honeypot param '#{default_honeypot}' was present.")
|
102
98
|
return true
|
103
99
|
end
|
104
100
|
end
|
@@ -107,8 +103,19 @@ module InvisibleCaptcha
|
|
107
103
|
false
|
108
104
|
end
|
109
105
|
|
110
|
-
def
|
106
|
+
def warn_spam(message)
|
111
107
|
logger.warn("Potential spam detected for IP #{request.remote_ip}. #{message}")
|
108
|
+
|
109
|
+
ActiveSupport::Notifications.instrument(
|
110
|
+
'invisible_captcha.spam_detected',
|
111
|
+
message: message,
|
112
|
+
remote_ip: request.remote_ip,
|
113
|
+
user_agent: request.user_agent,
|
114
|
+
controller: params[:controller],
|
115
|
+
action: params[:action],
|
116
|
+
url: request.url,
|
117
|
+
params: request.filtered_parameters
|
118
|
+
)
|
112
119
|
end
|
113
120
|
end
|
114
121
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module InvisibleCaptcha
|
2
4
|
module ViewHelpers
|
3
5
|
# Builds the honeypot html
|
@@ -54,7 +56,13 @@ module InvisibleCaptcha
|
|
54
56
|
|
55
57
|
return if visible
|
56
58
|
|
57
|
-
|
59
|
+
nonce = if Rails.version >= '5.2'
|
60
|
+
content_security_policy_nonce if options[:nonce]
|
61
|
+
else
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
|
65
|
+
content_tag(:style, media: 'screen', nonce: nonce) do
|
58
66
|
".#{css_class} {#{InvisibleCaptcha.css_strategy}}"
|
59
67
|
end
|
60
68
|
end
|
data/spec/controllers_spec.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
|
2
4
|
render_views
|
3
5
|
|
4
6
|
def switchable_post(action, params = {})
|
5
|
-
if
|
7
|
+
if Rails.version > '5'
|
6
8
|
post action, params: params
|
7
9
|
else
|
8
10
|
post action, params
|
@@ -10,7 +12,7 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
|
|
10
12
|
end
|
11
13
|
|
12
14
|
def switchable_put(action, params = {})
|
13
|
-
if
|
15
|
+
if Rails.version > '5'
|
14
16
|
put action, params: params
|
15
17
|
else
|
16
18
|
put action, params
|
@@ -63,12 +65,25 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
|
|
63
65
|
expect(session[:invisible_captcha_timestamp]).to be_nil
|
64
66
|
end
|
65
67
|
|
66
|
-
it '
|
68
|
+
it 'allows a custom on_timestamp_spam callback' do
|
67
69
|
switchable_put :update, id: 1, topic: { title: 'bar' }
|
68
70
|
|
69
71
|
expect(response.status).to eq(204)
|
70
72
|
end
|
71
73
|
|
74
|
+
it 'allows a new timestamp to be set in the on_timestamp_spam callback' do
|
75
|
+
@controller.singleton_class.class_eval do
|
76
|
+
def custom_timestamp_callback
|
77
|
+
session[:invisible_captcha_timestamp] = 2.seconds.from_now(Time.zone.now).iso8601
|
78
|
+
head(204)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
expect { switchable_put :update, id: 1, topic: { title: 'bar' } }
|
83
|
+
.to change { session[:invisible_captcha_timestamp] }
|
84
|
+
.to be_present
|
85
|
+
end
|
86
|
+
|
72
87
|
context 'successful submissions' do
|
73
88
|
it 'passes if submission on or after timestamp_threshold' do
|
74
89
|
sleep InvisibleCaptcha.timestamp_threshold
|
@@ -128,5 +143,43 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
|
|
128
143
|
expect(flash[:error]).not_to be_present
|
129
144
|
expect(@controller.params[:topic].key?(:subtitle)).to eq(false)
|
130
145
|
end
|
146
|
+
|
147
|
+
describe 'ActiveSupport::Notifications' do
|
148
|
+
let(:dummy_handler) { double(handle_event: nil) }
|
149
|
+
|
150
|
+
let!(:subscriber) do
|
151
|
+
subscriber = ActiveSupport::Notifications.subscribe('invisible_captcha.spam_detected') do |*args, data|
|
152
|
+
dummy_handler.handle_event(data)
|
153
|
+
end
|
154
|
+
|
155
|
+
subscriber
|
156
|
+
end
|
157
|
+
|
158
|
+
after { ActiveSupport::Notifications.unsubscribe(subscriber) }
|
159
|
+
|
160
|
+
it 'dispatches an `invisible_captcha.spam_detected` event' do
|
161
|
+
# Skip the `with` matcher for Rails < 5 due to issues comparing arguments passed to / recived by the dummy event handler.
|
162
|
+
# https://github.com/markets/invisible_captcha/pull/62#issuecomment-552218501
|
163
|
+
if Rails.version > '5'
|
164
|
+
expect(dummy_handler).to receive(:handle_event).once.with(
|
165
|
+
message: "Invisible Captcha honeypot param 'subtitle' was present.",
|
166
|
+
remote_ip: '0.0.0.0',
|
167
|
+
user_agent: 'Rails Testing',
|
168
|
+
controller: 'topics',
|
169
|
+
action: 'create',
|
170
|
+
url: 'http://test.host/topics',
|
171
|
+
params: {
|
172
|
+
topic: { subtitle: "foo"},
|
173
|
+
controller: 'topics',
|
174
|
+
action: 'create'
|
175
|
+
}
|
176
|
+
)
|
177
|
+
else
|
178
|
+
expect(dummy_handler).to receive(:handle_event).once
|
179
|
+
end
|
180
|
+
|
181
|
+
switchable_post :create, topic: { subtitle: 'foo' }
|
182
|
+
end
|
183
|
+
end
|
131
184
|
end
|
132
185
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
ENV['RAILS_ENV'] = 'test'
|
2
4
|
|
3
5
|
require File.expand_path("../dummy/config/environment.rb", __FILE__)
|
@@ -5,6 +7,9 @@ require 'rspec/rails'
|
|
5
7
|
require 'invisible_captcha'
|
6
8
|
|
7
9
|
RSpec.configure do |config|
|
10
|
+
if Rails.version >= '5.2'
|
11
|
+
config.include ActionDispatch::ContentSecurityPolicy::Request, type: :helper
|
12
|
+
end
|
8
13
|
config.disable_monkey_patching!
|
9
14
|
config.order = :random
|
10
15
|
config.expect_with :rspec
|
data/spec/view_helpers_spec.rb
CHANGED
@@ -1,8 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
RSpec.describe InvisibleCaptcha::ViewHelpers, type: :helper do
|
2
4
|
before(:each) do
|
3
5
|
allow(Time.zone).to receive(:now).and_return(Time.zone.parse('Feb 19 1986'))
|
4
6
|
allow(InvisibleCaptcha).to receive(:css_strategy).and_return("display:none;")
|
5
7
|
|
8
|
+
if Rails.version >= '5.2'
|
9
|
+
allow_any_instance_of(ActionDispatch::ContentSecurityPolicy::Request).to receive(:content_security_policy_nonce).and_return('123')
|
10
|
+
end
|
11
|
+
|
6
12
|
# to test content_for and provide
|
7
13
|
@view_flow = ActionView::OutputFlow.new
|
8
14
|
|
@@ -26,6 +32,12 @@ RSpec.describe InvisibleCaptcha::ViewHelpers, type: :helper do
|
|
26
32
|
expect(invisible_captcha(:subtitle, :topic, { class: 'foo_class' })).to match(/class="foo_class"/)
|
27
33
|
end
|
28
34
|
|
35
|
+
if Rails.version >= '5.2'
|
36
|
+
it 'with CSP nonce' do
|
37
|
+
expect(invisible_captcha(:subtitle, :topic, { nonce: true })).to match(/nonce="123"/)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
29
41
|
it 'generated html + styles' do
|
30
42
|
InvisibleCaptcha.honeypots = [:foo_id]
|
31
43
|
output = invisible_captcha.gsub("\"", "'")
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: invisible_captcha
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marc Anguera Insa
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-09-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: '4.2'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: '4.2'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rspec-rails
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,34 +52,6 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: test-unit
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - "~>"
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '3.0'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - "~>"
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '3.0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: byebug
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - ">="
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '0'
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - ">="
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '0'
|
83
55
|
description: Unobtrusive, flexible and simple spam protection for Rails applications
|
84
56
|
using honeypot strategy for better user experience.
|
85
57
|
email:
|
@@ -97,7 +69,6 @@ files:
|
|
97
69
|
- LICENSE
|
98
70
|
- README.md
|
99
71
|
- Rakefile
|
100
|
-
- gemfiles/rails_3.2.gemfile
|
101
72
|
- gemfiles/rails_4.2.gemfile
|
102
73
|
- gemfiles/rails_5.0.gemfile
|
103
74
|
- gemfiles/rails_5.1.gemfile
|
@@ -113,6 +84,7 @@ files:
|
|
113
84
|
- spec/controllers_spec.rb
|
114
85
|
- spec/dummy/README.md
|
115
86
|
- spec/dummy/Rakefile
|
87
|
+
- spec/dummy/app/assets/config/manifest.js
|
116
88
|
- spec/dummy/app/assets/javascripts/application.js
|
117
89
|
- spec/dummy/app/assets/stylesheets/application.css
|
118
90
|
- spec/dummy/app/controllers/application_controller.rb
|
@@ -180,6 +152,7 @@ test_files:
|
|
180
152
|
- spec/controllers_spec.rb
|
181
153
|
- spec/dummy/README.md
|
182
154
|
- spec/dummy/Rakefile
|
155
|
+
- spec/dummy/app/assets/config/manifest.js
|
183
156
|
- spec/dummy/app/assets/javascripts/application.js
|
184
157
|
- spec/dummy/app/assets/stylesheets/application.css
|
185
158
|
- spec/dummy/app/controllers/application_controller.rb
|