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 +4 -4
- data/lib/generators/spamtrap/install/templates/spamtrap.rb +3 -0
- data/lib/spamtrap/crypto.rb +18 -0
- data/lib/spamtrap/parameter_filter.rb +77 -0
- data/lib/spamtrap/railtie.rb +7 -0
- data/lib/spamtrap/version.rb +1 -1
- data/lib/spamtrap.rb +8 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1cc68536b7186bc9fac9249043e7bd3d0713b77070fab4611c08a8ba5fb1bfc1
|
|
4
|
+
data.tar.gz: 91c9ae78401af26537bc45c7c1ea17fac4245c264e1a6ee421fb6a9fd86af2d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
data/lib/spamtrap/crypto.rb
CHANGED
|
@@ -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
|
data/lib/spamtrap/railtie.rb
CHANGED
|
@@ -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
|
data/lib/spamtrap/version.rb
CHANGED
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.
|
|
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.
|
|
149
|
+
rubygems_version: 4.0.21
|
|
149
150
|
specification_version: 4
|
|
150
151
|
summary: Simple spamtrap for spambots.
|
|
151
152
|
test_files: []
|