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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81d808ca54efb58e16c61524bde0aee355f5a7b003ef4cb6cda2e9b672c7a284
4
- data.tar.gz: 6ec6f99dd02e353bb4e86bb9db8e65ecf35dc46e13051e401b62608700b7fde5
3
+ metadata.gz: 29b1886eff044771c56c4eeff7dfcc4a305262fd39b9633a43522e1a8a465f7a
4
+ data.tar.gz: 0ad53ba4ebdcec34064f33cc1ea2f75dc95ce6ffb57a733604174e0059c37521
5
5
  SHA512:
6
- metadata.gz: 17f8b181f15b009b850fea2e1ead3a5cb904218d8877b31d9b6a2e5f2578972a6a1a2837cdbda46013987911a88198d543dd7f3a79f68b455a969a005a3fbfc8
7
- data.tar.gz: 2b02561bf0516b2accb42ac64fe8d93b7bf956bb9474fcadec93db08f4be551d9eea430884d133d08b92156a9d10440ac8f48067207c95d3b72e4268752748dd
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 your Rails application against brute force attacks, DoS and bots.
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 some bots.
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
- The idea is to enable ActiveHashcash on sensitive forms such as login and registration. While the user is filling the form,
14
- ActiveHashcash performs the work in JavaScript and set the result into a hidden input text. The form cannot be submitted while the proof of work has not been found.
15
- The user submits the form, and the stamp is verified by the controller in a before action.
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. For the more sophisticated bots, we are happy to slow them down.
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
- Here is a [demo on a registration form](https://www.rorvswild.com/account/new) :
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
- ## Limitations
28
+ ---
24
29
 
25
- The JavaScript implementation is 10 to 20 times slower than the official C version.
26
- It needs some work and knowledges to be optimised. Unfortunately, I'm not a JavaScript expert.
27
- Maybe you have good JS skills to optimize it ?
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, either with `ActiveHashcash.bits = 20` or by overriding the method `hashcash_bits` in you controller.
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/[USERNAME]/active_hashcash.
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 [Base Secrète](https://basesecrete.com).
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
- config.after_initialize { load_translations }
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
@@ -1,3 +1,3 @@
1
1
  module ActiveHashcash
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -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
- def self.store=(store)
19
- @store = store
20
- end
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
- # TODO: protect_from_brute_force bits: 20, exception: ActionController::InvalidAuthenticityToken, with: :handle_failed_hashcash
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 :chech_hashcash, only: :create`
21
+ # Call me via a before_action when the form is submitted : `before_action :check_hashcash, only: :create`
25
22
  def check_hashcash
26
- stamp = hashcash_param && Stamp.parse(hashcash_param)
27
- if stamp && stamp.verify(hashcash_resource, hashcash_bits, Date.yesterday) && ActiveHashcash.store.add?(stamp)
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 of behaviour.
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
- # Define the complexity, the higher the slower it is. Consider lowering this value to not exclude people with old and slow devices.
43
- # On a decent laptop, it takes around 30 seconds for the JavaScript implementation to solve a 20 bits complexity and few seconds when it's 16.
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.bits
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"
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :active_hashcash do
3
+ # # Task goes here
4
+ # end
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.2.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: 2022-08-02 00:00:00.000000000 Z
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: ActiveHashcash protects your Rails application against brute force attacks,
42
- DoS and bots.
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: ActiveHashcash protects your Rails application against brute force attacks,
84
- DoS and bots.
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