active_hashcash 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +101 -14
- data/lib/active_hashcash/engine.rb +2 -14
- data/lib/active_hashcash/version.rb +1 -1
- data/lib/active_hashcash.rb +35 -21
- data/lib/tasks/active_hashcash_tasks.rake +4 -0
- metadata +10 -25
- data/lib/active_hashcash/stamp.rb +0 -52
- data/lib/active_hashcash/store.rb +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 29b1886eff044771c56c4eeff7dfcc4a305262fd39b9633a43522e1a8a465f7a
|
4
|
+
data.tar.gz: 0ad53ba4ebdcec34064f33cc1ea2f75dc95ce6ffb57a733604174e0059c37521
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 794687bc50403b03a63e4855451bec53751b6da4a20ef1674bdbf0d66dfdfacfaa68f3d1416a14a0a1bdf823b4641b8a16e6b70aba288278883170c7a73b6202
|
7
|
+
data.tar.gz: 7b0ca6f84cb75b2e49b5747034393c778023a2efb6974b97ba9fab084074da57cb78f5014de5d26e20af378bb8a86788f833f02e036e5feb56e8aee52c3b87d9
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
# Changelog of ActiveHashcash
|
2
|
+
|
3
|
+
## 0.3.0 - 2024-03-14
|
4
|
+
|
5
|
+
- Increase complexity automatically to slowdown brute force attacks
|
6
|
+
- Add mountable dashboard to list latest stamps and most frequent IP addresses
|
7
|
+
- Store stamps into the database instead of Redis
|
8
|
+
- Fix ActiveHashcash::Store#add? by converting stamp to a string
|
9
|
+
|
1
10
|
## 0.2.0 - 2022-08-02
|
2
11
|
|
3
12
|
- Add ActiveHashcash::Store#clean to removed expired stamps
|
data/README.md
CHANGED
@@ -2,29 +2,34 @@
|
|
2
2
|
|
3
3
|
<img align="right" width="200px" src="logo.png" alt="Active Hashcash logo"/>
|
4
4
|
|
5
|
-
ActiveHashcash protects
|
5
|
+
ActiveHashcash protects Rails applications against bots and brute force attacks without annoying humans.
|
6
6
|
|
7
7
|
Hashcash is proof-of-work algorithm, invented by Adam Back in 1997, to protect systems against denial of service attacks.
|
8
|
-
ActiveHashcash is an easy way to protect any Rails application against brute force attacks and
|
8
|
+
ActiveHashcash is an easy way to protect any Rails application against brute force attacks and bots.
|
9
9
|
|
10
10
|
The idea is to force clients to spend some time to solve a hard problem that is very easy to verify for the server.
|
11
11
|
We have developped ActiveHashcash after seeing brute force attacks against our Rails application monitoring service [RorVsWild](https://rorvswild.com).
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
The
|
13
|
+
ActiveHashcash is ideal to set up on sensitive forms such as login and registration.
|
14
|
+
While the user is filling the form, the problem is solved in JavaScript and set the result into a hidden input text.
|
15
|
+
The form cannot be submitted while the proof of work has not been found.
|
16
|
+
Then the user submits the form, and the stamp is verified by the controller in a before action.
|
16
17
|
|
17
|
-
It blocks bots that do not interpret JavaScript since the proof of work is not computed.
|
18
|
+
It blocks bots that do not interpret JavaScript since the proof of work is not computed.
|
19
|
+
More sophisticated bots and brute force attacks are slow down.
|
20
|
+
Moreover the complexity increases automatically for IP addresses sending many requests.
|
21
|
+
Thus it becomes very CPU costly for attackers.
|
18
22
|
|
19
|
-
|
23
|
+
Finally legitimate users are not annoyed by asking to solve a puzzle or clicking on the all images containing a bus.
|
24
|
+
Here is a [demo on a registration form](https://www.rorvswild.com/session) :
|
20
25
|
|
21
26
|
![Active Hashcash GIF preview](demo.gif)
|
22
27
|
|
23
|
-
|
28
|
+
---
|
24
29
|
|
25
|
-
|
26
|
-
|
27
|
-
|
30
|
+
<img align="left" height="24px" src="rorvswild_logo.jpg" alt="RorVsWild logo"/>Made by <a href="https://www.rorvswild.com">RorVsWild</a>, performances & exceptions monitoring for Ruby on Rails applications.
|
31
|
+
|
32
|
+
---
|
28
33
|
|
29
34
|
## Installation
|
30
35
|
|
@@ -62,26 +67,108 @@ end
|
|
62
67
|
To customize some behaviour, you can override most of the methods which begins with `hashcash_`.
|
63
68
|
Simply have a look to `active_hashcash.rb`.
|
64
69
|
|
70
|
+
Stamps are stored into into the database to prevents from spending them more than once.
|
71
|
+
You must run a migration:
|
72
|
+
|
73
|
+
```
|
74
|
+
rails active_hashcash:install:migrations
|
75
|
+
rails db:migrate
|
76
|
+
```
|
77
|
+
|
78
|
+
### Dashboard
|
79
|
+
|
80
|
+
There is a mountable dahsboard which allows to see all spent stamps.
|
81
|
+
It's not mandatory, but useful for monitoring purpose.
|
82
|
+
|
83
|
+
![ActiveHashcash dashboard](active_hashcash_dashboard.png "ActiveHashcash dashboard")
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
# config/routes.rb
|
87
|
+
mount ActiveHashcash::Engine, at: "hashcash"
|
88
|
+
```
|
89
|
+
|
90
|
+
ActiveHashcash cannot guess how you handle user authentication, because it is different for all Rails applications.
|
91
|
+
So you have to monkey patch `ActiveHashcash::ApplicationController` in order to inject your own mechanism.
|
92
|
+
The patch can be saved wherever you want.
|
93
|
+
For example, I like to have all the patches in one place, so I put them in `lib/patches`.
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
# lib/patches/active_hashcash.rb
|
97
|
+
|
98
|
+
ActiveHashcash::ApplicationController.class_eval do
|
99
|
+
before_action :require_admin
|
100
|
+
|
101
|
+
def require_admin
|
102
|
+
# This example supposes there are current_user and User#admin? methods
|
103
|
+
raise ActionController::RoutingError.new("Not found") unless current_user.try(:admin?)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
Then you have to require the monkey patch.
|
110
|
+
Because it's loaded via require, it won't be reloaded in development.
|
111
|
+
Since you are not supposed to change this file often, it should not be an issue.
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
# config/application.rb
|
115
|
+
config.after_initialize do
|
116
|
+
require "patches/active_hashcash"
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
If you use Devise, you can check the permission directly from routes.rb:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
# config/routes.rb
|
124
|
+
authenticate :user, -> (u) { u.admin? } do # Supposing there is a User#admin? method
|
125
|
+
mount ActiveHashcash::Engine, at: "hashcash" # http://localhost:3000/hashcash
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
### Before version 0.3.0
|
130
|
+
|
65
131
|
You must have Redis in order to prevent double spent stamps. Otherwise it will be useless.
|
66
132
|
It automatically tries to connect with the environement variables `ACTIVE_HASHCASH_REDIS_URL` or `REDIS_URL`.
|
67
133
|
You can also manually set the URL with `ActiveHashcash.redis_url = redis://user:password@localhost:6379`.
|
68
134
|
|
69
135
|
You should call `ActiveHashcash::Store#clean` once a day, to remove expired stamps.
|
70
136
|
|
137
|
+
To upgrade from 0.2.0 you must run the migration :
|
138
|
+
|
139
|
+
```
|
140
|
+
rails active_hashcash:install:migrations
|
141
|
+
rails db:migrate
|
142
|
+
```
|
143
|
+
|
71
144
|
## Complexity
|
72
145
|
|
73
146
|
Complexity is the most important parameter. By default its value is 20 and requires most of the time 5 to 20 seconds to be solved on a decent laptop.
|
74
147
|
The user won't wait that long, since he needs to fill the form while the problem is solving.
|
75
148
|
Howevever, if your application includes people with slow and old devices, then consider lowering this value, to 16 or 18.
|
76
149
|
|
77
|
-
You can change the complexity
|
150
|
+
You can change the minimum complexity with `ActiveHashcash.bits = 20`.
|
151
|
+
|
152
|
+
Since version 0.3.0, the complexity increases with the number of stamps spent during le last 24H from the same IP address.
|
153
|
+
Thus it becomes very efficient to slow down brute force attacks.
|
154
|
+
|
155
|
+
## Limitations
|
156
|
+
|
157
|
+
The JavaScript implementation is 10 to 20 times slower than the official C version.
|
158
|
+
I first used the SubtleCrypto API but it is surprisingly slower than a custom SHA1 implementation.
|
159
|
+
Maybe I did in an unefficient way 2df3ba5?
|
160
|
+
Another idea would be to compile the work algorithm in wasm.
|
161
|
+
|
162
|
+
Unfortunately, I'm not a JavaScript expert.
|
163
|
+
Maybe you have good JS skills to optimize it?
|
164
|
+
Any help would be appreciate to better fights bots and brute for attacks!
|
78
165
|
|
79
166
|
## Contributing
|
80
167
|
|
81
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
168
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/BaseSecrete/active_hashcash.
|
82
169
|
|
83
170
|
## License
|
84
171
|
|
85
172
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
86
173
|
|
87
|
-
Made by Alexis Bernard at [
|
174
|
+
Made by Alexis Bernard at [RorVsWild](https://www.rorvswild.com).
|
@@ -1,19 +1,7 @@
|
|
1
1
|
module ActiveHashcash
|
2
2
|
class Engine < ::Rails::Engine
|
3
|
-
config.assets.paths << File.expand_path("../..", __FILE__)
|
3
|
+
config.assets.paths << File.expand_path("../..", __FILE__) if config.respond_to?(:assets)
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
def load_translations
|
8
|
-
if !I18n.backend.exists?(I18n.locale, "active_hashcash")
|
9
|
-
I18n.backend.store_translations(:de, {active_hashcash: {waiting_label: "Warten auf die Überprüfung ..."}})
|
10
|
-
I18n.backend.store_translations(:en, {active_hashcash: {waiting_label: "Waiting for verification ..."}})
|
11
|
-
I18n.backend.store_translations(:es, {active_hashcash: {waiting_label: "A la espera de la verificación ..."}})
|
12
|
-
I18n.backend.store_translations(:fr, {active_hashcash: {waiting_label: "En attente de vérification ..."}})
|
13
|
-
I18n.backend.store_translations(:it, {active_hashcash: {waiting_label: "In attesa di verifica ..."}})
|
14
|
-
I18n.backend.store_translations(:jp, {active_hashcash: {waiting_label: "検証待ち ..."}})
|
15
|
-
I18n.backend.store_translations(:pt, {active_hashcash: {waiting_label: "À espera de verificação ..."}})
|
16
|
-
end
|
17
|
-
end
|
5
|
+
isolate_namespace ActiveHashcash
|
18
6
|
end
|
19
7
|
end
|
data/lib/active_hashcash.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
require "active_hashcash/version"
|
2
|
+
require "active_hashcash/engine"
|
3
|
+
|
1
4
|
module ActiveHashcash
|
2
5
|
extend ActiveSupport::Concern
|
3
6
|
|
@@ -7,31 +10,41 @@ module ActiveHashcash
|
|
7
10
|
helper_method :hashcash_hidden_field_tag
|
8
11
|
end
|
9
12
|
|
10
|
-
mattr_accessor :bits, instance_accessor: false, default: 20
|
11
13
|
mattr_accessor :resource, instance_accessor: false
|
12
|
-
mattr_accessor :redis_url, instance_accessor: false, default: ENV["ACTIVE_HASHCASH_REDIS_URL"] || ENV["REDIS_URL"]
|
13
|
-
|
14
|
-
def self.store
|
15
|
-
@store ||= Store.new(Redis.new(url: ActiveHashcash.redis_url))
|
16
|
-
end
|
17
14
|
|
18
|
-
|
19
|
-
|
20
|
-
|
15
|
+
# This is base complexity.
|
16
|
+
# Consider lowering it to not exclude people with old and slow devices.
|
17
|
+
mattr_accessor :bits, instance_accessor: false, default: 16
|
21
18
|
|
22
|
-
|
19
|
+
mattr_accessor :date_format, instance_accessor: false, default: "%y%m%d"
|
23
20
|
|
24
|
-
# Call me via a before_action when the form is submitted : `before_action :
|
21
|
+
# Call me via a before_action when the form is submitted : `before_action :check_hashcash, only: :create`
|
25
22
|
def check_hashcash
|
26
|
-
|
27
|
-
|
23
|
+
attrs = {
|
24
|
+
ip_address: hashcash_ip_address,
|
25
|
+
request_path: hashcash_request_path,
|
26
|
+
context: hashcash_stamp_context
|
27
|
+
}
|
28
|
+
if hashcash_param && Stamp.spend(hashcash_param, hashcash_resource, hashcash_bits, Date.yesterday, attrs)
|
28
29
|
hashcash_after_success
|
29
30
|
else
|
30
31
|
hashcash_after_failure
|
31
32
|
end
|
32
33
|
end
|
33
34
|
|
34
|
-
# Override the methods below in your controller, to change any parameter
|
35
|
+
# Override the methods below in your controller, to change any parameter or behaviour.
|
36
|
+
|
37
|
+
def hashcash_ip_address
|
38
|
+
request.remote_ip
|
39
|
+
end
|
40
|
+
|
41
|
+
def hashcash_request_path
|
42
|
+
request.path
|
43
|
+
end
|
44
|
+
|
45
|
+
def hashcash_stamp_context
|
46
|
+
# Override this method to store custom data for each stamp
|
47
|
+
end
|
35
48
|
|
36
49
|
# By default the host name is used as the resource.
|
37
50
|
# It' should be good for most cases and prevent from reusing the same stamp between sites.
|
@@ -39,10 +52,15 @@ module ActiveHashcash
|
|
39
52
|
ActiveHashcash.resource || request.host
|
40
53
|
end
|
41
54
|
|
42
|
-
#
|
43
|
-
#
|
55
|
+
# Returns the complexity, the higher the slower it is.
|
56
|
+
# Complexity is increased logarithmicly for each IP during the last 24H to slowdown brute force attacks.
|
57
|
+
# The minimun value returned is `ActiveHashcash.bits`.
|
44
58
|
def hashcash_bits
|
45
|
-
ActiveHashcash.
|
59
|
+
if (previous_stamp_count = ActiveHashcash::Stamp.where(ip_address: hashcash_ip_address).where(created_at: 1.day.ago..).count) > 0
|
60
|
+
(ActiveHashcash.bits + Math.log2(previous_stamp_count)).floor
|
61
|
+
else
|
62
|
+
ActiveHashcash.bits
|
63
|
+
end
|
46
64
|
end
|
47
65
|
|
48
66
|
# Override if you want to rename the hashcash param.
|
@@ -72,7 +90,3 @@ module ActiveHashcash
|
|
72
90
|
hidden_field_tag(name, "", "data-hashcash" => options.to_json)
|
73
91
|
end
|
74
92
|
end
|
75
|
-
|
76
|
-
require "active_hashcash/stamp"
|
77
|
-
require "active_hashcash/store"
|
78
|
-
require "active_hashcash/engine"
|
metadata
CHANGED
@@ -1,29 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_hashcash
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexis Bernard
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-03-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: redis
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: 4.0.0
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: 4.0.0
|
27
13
|
- !ruby/object:Gem::Dependency
|
28
14
|
name: rails
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,8 +24,8 @@ dependencies:
|
|
38
24
|
- - ">="
|
39
25
|
- !ruby/object:Gem::Version
|
40
26
|
version: 5.2.0
|
41
|
-
description:
|
42
|
-
|
27
|
+
description: Protect Rails applications against bots and brute force attacks without
|
28
|
+
annoying humans.
|
43
29
|
email:
|
44
30
|
- alexis@basesecrete.com
|
45
31
|
executables: []
|
@@ -51,10 +37,9 @@ files:
|
|
51
37
|
- README.md
|
52
38
|
- lib/active_hashcash.rb
|
53
39
|
- lib/active_hashcash/engine.rb
|
54
|
-
- lib/active_hashcash/stamp.rb
|
55
|
-
- lib/active_hashcash/store.rb
|
56
40
|
- lib/active_hashcash/version.rb
|
57
41
|
- lib/hashcash.js
|
42
|
+
- lib/tasks/active_hashcash_tasks.rake
|
58
43
|
homepage: https://github.com/BaseSecrete/active_hashcash
|
59
44
|
licenses:
|
60
45
|
- MIT
|
@@ -62,7 +47,7 @@ metadata:
|
|
62
47
|
homepage_uri: https://github.com/BaseSecrete/active_hashcash
|
63
48
|
source_code_uri: https://github.com/BaseSecrete/active_hashcash
|
64
49
|
changelog_uri: https://github.com/BaseSecrete/active_hashcash/CHANGELOG.md
|
65
|
-
post_install_message:
|
50
|
+
post_install_message:
|
66
51
|
rdoc_options: []
|
67
52
|
require_paths:
|
68
53
|
- lib
|
@@ -78,8 +63,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
78
63
|
version: '0'
|
79
64
|
requirements: []
|
80
65
|
rubygems_version: 3.2.22
|
81
|
-
signing_key:
|
66
|
+
signing_key:
|
82
67
|
specification_version: 4
|
83
|
-
summary:
|
84
|
-
|
68
|
+
summary: Protect Rails applications against bots and brute force attacks without annoying
|
69
|
+
humans.
|
85
70
|
test_files: []
|
@@ -1,52 +0,0 @@
|
|
1
|
-
module ActiveHashcash
|
2
|
-
class Stamp
|
3
|
-
attr_reader :version, :bits, :date, :resource, :extension, :rand, :counter
|
4
|
-
|
5
|
-
def self.parse(string)
|
6
|
-
args = string.split(":")
|
7
|
-
new(args[0], args[1], args[2], args[3], args[4], args[5], args[6])
|
8
|
-
end
|
9
|
-
|
10
|
-
def self.mint(resource, options = {})
|
11
|
-
new(
|
12
|
-
options[:version] || 1,
|
13
|
-
options[:bits] || ActiveHashcash.bits,
|
14
|
-
options[:date] || Date.today.strftime("%y%m%d"),
|
15
|
-
resource,
|
16
|
-
options[:ext],
|
17
|
-
options[:rand] || SecureRandom.alphanumeric(16),
|
18
|
-
options[:counter] || 0).work
|
19
|
-
end
|
20
|
-
|
21
|
-
def initialize(version, bits, date, resource, extension, rand, counter)
|
22
|
-
@version = version
|
23
|
-
@bits = bits.to_i
|
24
|
-
@date = date.respond_to?(:strftime) ? date.strftime("%y%m%d") : date
|
25
|
-
@resource = resource
|
26
|
-
@extension = extension
|
27
|
-
@rand = rand
|
28
|
-
@counter = counter
|
29
|
-
end
|
30
|
-
|
31
|
-
def valid?
|
32
|
-
Digest::SHA1.hexdigest(to_s).hex >> (160-bits) == 0
|
33
|
-
end
|
34
|
-
|
35
|
-
def verify(resource, bits, date)
|
36
|
-
self.resource == resource && self.bits >= bits && parse_date >= date && valid?
|
37
|
-
end
|
38
|
-
|
39
|
-
def to_s
|
40
|
-
[version, bits, date, resource, extension, rand, counter].join(":")
|
41
|
-
end
|
42
|
-
|
43
|
-
def parse_date
|
44
|
-
Date.strptime(date, "%y%m%d")
|
45
|
-
end
|
46
|
-
|
47
|
-
def work
|
48
|
-
@counter += 1 until valid?
|
49
|
-
self
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
@@ -1,25 +0,0 @@
|
|
1
|
-
module ActiveHashcash
|
2
|
-
class Store
|
3
|
-
attr_reader :redis
|
4
|
-
|
5
|
-
def initialize(redis = Redis.new(url: ActiveHashcash.redis_url || ENV["ACTIVE_HASHCASH_REDIS_URL"] || ENV["REDIS_URL"]))
|
6
|
-
@redis = redis
|
7
|
-
end
|
8
|
-
|
9
|
-
def add?(stamp)
|
10
|
-
redis.sadd("active_hashcash_stamps_#{stamp.date}", stamp) ? self : nil
|
11
|
-
end
|
12
|
-
|
13
|
-
def clear
|
14
|
-
redis.del(redis.keys("active_hashcash_stamps*"))
|
15
|
-
end
|
16
|
-
|
17
|
-
def clean
|
18
|
-
today = Date.today.strftime("%y%m%d")
|
19
|
-
yesterday = (Date.today - 1).strftime("%y%m%d")
|
20
|
-
keep = ["active_hashcash_stamps_#{today}", "active_hashcash_stamps_#{yesterday}"]
|
21
|
-
keys = redis.keys("active_hashcash_stamps*")
|
22
|
-
redis.del(keys - keep)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|