spamtrap 0.4.2 → 0.4.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a31cf3397317c7e4fadc471f44ce713dac53bea0701ba50b24a8259a77ed4b58
4
- data.tar.gz: 3c3f3f0119ae22e79a3e327ae5aa1c65b915b906c2bb8b5e3621e536d62ccab2
3
+ metadata.gz: 1cc68536b7186bc9fac9249043e7bd3d0713b77070fab4611c08a8ba5fb1bfc1
4
+ data.tar.gz: 91c9ae78401af26537bc45c7c1ea17fac4245c264e1a6ee421fb6a9fd86af2d0
5
5
  SHA512:
6
- metadata.gz: 9abc563f9854fb5759371ac2973b3071b3cc86c4cab3af2566aed9b491079a2870b1df82e1dd5cf561f107a88786a818c42d294678e454cce86522cdabfe4ae0
7
- data.tar.gz: 6d4b395ebb6d81a2a002919935001a77a7febdf3353fa5556de2ccd29883ecfe5f29e21b63de448ff53ccfb0233b85708547892cd089123a843775a9aace449d
6
+ metadata.gz: 5985f1e8600a0f2d361b94ede13e567ea8b09d8e9ff3d7b4b0c2be99987d50d85bf5271dcea1bf3a0b5cc128a0fc066d6395b8ebf0be53dbc25970333e8ddab0
7
+ data.tar.gz: 5adec9413ed191d8de06866db16da6b68b65ead9509749a535fe416964540da1751037f44e52a688461cd828cbe82826d4714c40d08c06fdafa8f68e0b2f07da
@@ -41,6 +41,9 @@
41
41
  # Whether mutated fields keep an id/for derived from the real field name.
42
42
  # Spamtrap.stable_ids = true
43
43
 
44
+ # Apply config.filter_parameters to mutated field names, which the log otherwise shows unfiltered.
45
+ # Spamtrap.filter_parameters = true
46
+
44
47
  # Fill time
45
48
 
46
49
  # Minimum seconds between render and submission a human needs; false or 0 disables.
@@ -25,6 +25,24 @@ module Spamtrap
25
25
  derived
26
26
  end
27
27
  end
28
+
29
+ # The name a mutated token decrypts to under each secret, skipping the tag and AAD check
30
+ # because Rails filters params before token_context can be computed. Unverified: fit only
31
+ # for deciding whether to mask a logged value, never for reading params.
32
+ def unverified_field_names(token)
33
+ raw = Base64.urlsafe_decode64(token)
34
+ return [] unless raw.bytesize > NONCE_LEN + TAG_LEN
35
+
36
+ [Spamtrap.secret_key_base, Spamtrap.previous_secret_key_base].compact.filter_map do |secret|
37
+ cipher = OpenSSL::Cipher.new(CIPHER).decrypt
38
+ cipher.key = keys_for(secret)[:mutation]
39
+ cipher.iv = raw[0, NONCE_LEN]
40
+ name = cipher.update(raw[NONCE_LEN...-TAG_LEN]).force_encoding(Encoding::UTF_8)
41
+ name if name.valid_encoding? # a wrong secret yields random bytes, rarely valid UTF-8
42
+ end
43
+ rescue ArgumentError
44
+ []
45
+ end
28
46
  end
29
47
 
30
48
  private
@@ -0,0 +1,77 @@
1
+ module Spamtrap
2
+ # Rails logs params before any before_action runs, so mutated field names reach the log still
3
+ # encrypted and never match the app's filter_parameters. The Railtie appends BLOCK to
4
+ # filter_parameters; it decrypts each name and applies the app's own filters to the real one.
5
+ module ParameterFilter
6
+ # base64url of IV + at least one name byte + tag, then a date/time select's (Ni) suffix.
7
+ TOKEN = /\A([A-Za-z0-9_-]{39,})(\(\d+[a-z]\))?\z/
8
+
9
+ # Token key => parent paths, built once per params hash; weak so logged params aren't retained.
10
+ PARENTS = ObjectSpace::WeakMap.new
11
+
12
+ # Rails only calls blocks for leaf values no key filter matched, passing a copy of the
13
+ # value to mutate and the top-level params.
14
+ BLOCK = lambda do |key, value, params|
15
+ # Cheapest checks first: this runs for every leaf param of every request Rails filters.
16
+ next unless Spamtrap.filter_parameters && params.respond_to?(:key?)
17
+ next unless params.key?('spamtrap_timestamp') || params.key?(:spamtrap_timestamp)
18
+ next unless value.is_a?(String) && (token = TOKEN.match(key.to_s))
19
+
20
+ masked = Spamtrap::ParameterFilter.mask(token, value, params)
21
+ value.replace(masked) if masked
22
+ end
23
+
24
+ class << self
25
+ # What the app's filters turn value into at the field's real path, or nil if they leave it.
26
+ # Only leaf names are mutated, so the containers above the token give the real path. A token
27
+ # shared by fields_for children has several parents; masking if any path would is the safe side.
28
+ def mask(token, value, params)
29
+ filter = app_filter
30
+ parents = (PARENTS[params] ||= parent_paths(params)).fetch(token[0], [[]])
31
+ Spamtrap::Crypto.unverified_field_names(token[1]).each do |name|
32
+ parents.each do |parent|
33
+ path = [*parent, "#{name}#{token[2]}"]
34
+ result = path.inject(filter.filter(nest(path, value.dup))) { |node, key| node.is_a?(Hash) ? node[key] : node }
35
+ return result.to_s unless result == value
36
+ end
37
+ end
38
+ nil
39
+ end
40
+
41
+ # The app's filters without BLOCK itself, rebuilt when the list is reassigned or grows.
42
+ def app_filter
43
+ filters = Rails.application.config.filter_parameters
44
+ signature = [filters.object_id, filters.size]
45
+ return @app_filter if @app_filter_signature == signature
46
+
47
+ # Filter before signature, so a concurrent reader never pairs the new signature with the old filter.
48
+ @app_filter = ActiveSupport::ParameterFilter.new(filters.reject { |f| f.equal?(BLOCK) })
49
+ @app_filter_signature = signature
50
+ @app_filter
51
+ end
52
+
53
+ private
54
+
55
+ # Array positions are left out of a path, as Rails leaves them out of dotted filter matching.
56
+ def parent_paths(params)
57
+ paths = Hash.new { |hash, key| hash[key] = [] }
58
+ visit = lambda do |node, path|
59
+ if node.is_a?(Array)
60
+ node.each { |element| visit.call(element, path) }
61
+ elsif node.is_a?(Hash)
62
+ node.each do |key, child|
63
+ paths[key.to_s] |= [path] if TOKEN.match?(key.to_s)
64
+ visit.call(child, [*path, key.to_s])
65
+ end
66
+ end
67
+ end
68
+ visit.call(params, [])
69
+ paths
70
+ end
71
+
72
+ def nest(path, value)
73
+ path.reverse.inject(value) { |inner, key| { key => inner } }
74
+ end
75
+ end
76
+ end
77
+ end
@@ -9,5 +9,12 @@ module Spamtrap
9
9
  # miss the on_load hook, so patch now if the form helpers are already loaded.
10
10
  Spamtrap.install_form_builder! if Spamtrap.form_builder_loaded?
11
11
  end
12
+
13
+ # After the app's initializers, so one that assigns filter_parameters can't drop the block.
14
+ # Always installed; Spamtrap.filter_parameters switches it at request time.
15
+ initializer 'spamtrap.filter_parameters', after: :load_config_initializers do |app|
16
+ filters = app.config.filter_parameters
17
+ filters << Spamtrap::ParameterFilter::BLOCK unless filters.include?(Spamtrap::ParameterFilter::BLOCK)
18
+ end
12
19
  end
13
20
  end
@@ -1,5 +1,5 @@
1
1
  module Spamtrap
2
2
 
3
- VERSION = '0.4.2'
3
+ VERSION = '0.4.3'
4
4
 
5
5
  end
data/lib/spamtrap.rb CHANGED
@@ -63,6 +63,13 @@ module Spamtrap
63
63
  @enabled.nil? ? true : @enabled
64
64
  end
65
65
 
66
+ attr_writer :filter_parameters
67
+
68
+ # Apply the app's filter_parameters to mutated field names in the log. Default true.
69
+ def filter_parameters
70
+ @filter_parameters.nil? ? true : @filter_parameters
71
+ end
72
+
66
73
  attr_writer :nonce_bind_ip
67
74
 
68
75
  # true binds the nonce to the full client IP (default), :prefix to its /24 or /48
@@ -185,5 +192,6 @@ module Spamtrap
185
192
  require 'spamtrap/crypto'
186
193
  require 'spamtrap/controller'
187
194
  require 'spamtrap/helper'
195
+ require 'spamtrap/parameter_filter'
188
196
  require 'spamtrap/railtie' if defined?(Rails::Railtie)
189
197
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spamtrap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cedric Howe
@@ -124,6 +124,7 @@ files:
124
124
  - lib/spamtrap/controller.rb
125
125
  - lib/spamtrap/crypto.rb
126
126
  - lib/spamtrap/helper.rb
127
+ - lib/spamtrap/parameter_filter.rb
127
128
  - lib/spamtrap/railtie.rb
128
129
  - lib/spamtrap/test_helper.rb
129
130
  - lib/spamtrap/version.rb
@@ -145,7 +146,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
145
146
  - !ruby/object:Gem::Version
146
147
  version: 1.3.6
147
148
  requirements: []
148
- rubygems_version: 4.0.20
149
+ rubygems_version: 4.0.21
149
150
  specification_version: 4
150
151
  summary: Simple spamtrap for spambots.
151
152
  test_files: []