ree_lib 1.3.15 → 1.3.16

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: 2ecb6d379981355aad60ca8e7f67493643cc351e3f5232c2d8baa326a7630d07
4
- data.tar.gz: 2530c228029eb4df4e96d6208f7a0c13c22aeb263c573e69372683f4f195449f
3
+ metadata.gz: 7b6bab291292300f6d1067e79262021b1a1ce40474fd4d3baf2ddec2398c521f
4
+ data.tar.gz: fe0536bc647d3bd48cbc04197a61fc5415aa8c58fb0aa1578eb90eafaa1c424c
5
5
  SHA512:
6
- metadata.gz: b1cc7a1ffbb0a4946d8359bb24ac0340c567fe9631dd389c70dc78fba3b5812d963d7174352a0e50ea7c918e7004a2ffa9c989797673978687c7865199179eb0
7
- data.tar.gz: 815c690e32475a28c02dc3bbf8b4b8808b0b23302dd0cb8e5f35322b688f26ed68f4e1d98a0c4e10a9b6d80194a5fafec15fa76d8a0e60895b394918c4446754
6
+ metadata.gz: 40ec71033ebe98320f43b20cf2dd626272cc313ee3d28540fd333d8aabf403f38c87c502c174184331aa6ca7e44cc6b657e5854a740f75eff844c6e9ab2f64b7
7
+ data.tar.gz: 03433f0fd291d8709afd41856ed2141e1b418ee39d72ce19148390c86660a7a13411e99f20e0d2a98dac5d37d96fbf05fba845f0ba8b6ae7e1c395e074fc381a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.3.16] - 2026-10-21
4
+
5
+ - `ree_roda`: the `ree_logger` plugin applies `filter_params` to the query
6
+ string of the `Request:` line. It used to be logged verbatim, so a secret
7
+ sent in the URL reached the log even though `Params` showed it filtered.
8
+ A bracketed name (`user[password]`, also percent-encoded) is matched by
9
+ each of its segments.
10
+ - `ree_roda`: `filter_params` in the `ree_logger` plugin now walks nested
11
+ hashes and arrays; it used to check top level keys only. Names are still
12
+ matched exactly.
13
+
3
14
  ## [1.3.12] - 2026-09-02
4
15
 
5
16
  - `ree_audit`: `annotate_audit` fn — the entry point product code uses to attach
data/Gemfile.lock CHANGED
@@ -9,7 +9,7 @@ PATH
9
9
  PATH
10
10
  remote: .
11
11
  specs:
12
- ree_lib (1.3.15)
12
+ ree_lib (1.3.16)
13
13
  bigdecimal
14
14
  binding_of_caller
15
15
  i18n
@@ -1,3 +1,4 @@
1
+ require "uri"
1
2
  package_require "ree_logger/beans/logger"
2
3
 
3
4
  class Roda
@@ -50,7 +51,7 @@ class Roda
50
51
 
51
52
  message = <<~DOC
52
53
  Request/Response details:
53
- Request: #{env["REQUEST_METHOD"]} #{env["QUERY_STRING"] && !env["QUERY_STRING"].empty? ? env["SCRIPT_NAME"].to_s + env["PATH_INFO"] + "?#{env["QUERY_STRING"]}": env["SCRIPT_NAME"].to_s + env["PATH_INFO"]} #{opts[:ree_logger_log_params] ? "\n Params: " + _ree_logger_filtered_params.inspect : ""}
54
+ Request: #{env["REQUEST_METHOD"]} #{_ree_logger_request_path(env)} #{opts[:ree_logger_log_params] ? "\n Params: " + _ree_logger_filtered_params.inspect : ""}
54
55
  Response status: #{response.status || "-"}#{(400..499).include?(response.status) ? "\n Response body: " + response.body[0].to_s : ""}
55
56
  Time Taken: #{elapsed_time}
56
57
  DOC
@@ -64,11 +65,69 @@ class Roda
64
65
 
65
66
  return params if filter_keys.empty?
66
67
 
68
+ _ree_logger_filter_value(params, filter_keys, opts[:ree_logger_filter_params_placeholder])
69
+ end
70
+
71
+ # Keys are matched by exact name at any depth: a password nested in
72
+ # `{user: {password: ...}}` is still a password. Exact match, unlike the
73
+ # substring match of ree_audit, keeps `token_id` and the like readable.
74
+ def _ree_logger_filter_value(value, filter_keys, placeholder)
75
+ case value
76
+ when Hash
77
+ value.each_with_object({}) do |(k, v), h|
78
+ h[k] = if filter_keys.include?(k.to_s)
79
+ placeholder
80
+ else
81
+ _ree_logger_filter_value(v, filter_keys, placeholder)
82
+ end
83
+ end
84
+ when Array
85
+ value.map { _ree_logger_filter_value(_1, filter_keys, placeholder) }
86
+ else
87
+ value
88
+ end
89
+ end
90
+
91
+ def _ree_logger_request_path(env)
92
+ path = env["SCRIPT_NAME"].to_s + env["PATH_INFO"].to_s
93
+ query = env["QUERY_STRING"]
94
+
95
+ return path if query.nil? || query.empty?
96
+
97
+ "#{path}?#{_ree_logger_filtered_query(query)}"
98
+ end
99
+
100
+ # The query string is logged verbatim next to the path, so without this
101
+ # a secret sent in the URL (OAuth callback code, a token from an email
102
+ # link) reached the log even though Params showed it as filtered.
103
+ def _ree_logger_filtered_query(query)
104
+ filter_keys = opts[:ree_logger_filter_params]
105
+
106
+ return query if filter_keys.empty?
107
+
67
108
  placeholder = opts[:ree_logger_filter_params_placeholder]
68
109
 
69
- params.each_with_object({}) do |(k, v), h|
70
- h[k] = filter_keys.include?(k.to_s) ? placeholder : v
110
+ query.split("&", -1).map do |pair|
111
+ key, value = pair.split("=", 2)
112
+
113
+ if value && _ree_logger_filtered_query_key?(key, filter_keys)
114
+ "#{key}=#{placeholder}"
115
+ else
116
+ pair
117
+ end
118
+ end.join("&")
119
+ end
120
+
121
+ # `user[password]=...` is the nested form of the same param, so every
122
+ # bracket segment of the name is checked.
123
+ def _ree_logger_filtered_query_key?(key, filter_keys)
124
+ name = begin
125
+ URI.decode_www_form_component(key)
126
+ rescue ArgumentError
127
+ key
71
128
  end
129
+
130
+ name.scan(/[^\[\]]+/).any? { filter_keys.include?(_1) }
72
131
  end
73
132
 
74
133
  # Create timer instance used for timing
@@ -3,34 +3,107 @@
3
3
  package_require("ree_roda/plugins/ree_logger")
4
4
 
5
5
  RSpec.describe :ree_logger do
6
- it "filters request params" do
7
- class TestFilterApp < ReeRoda::App
6
+ class TestFilterLogger
7
+ def info(msg)
8
+ logs << msg
9
+ end
10
+
11
+ def logs
12
+ @logs ||= []
13
+ end
14
+ end
15
+
16
+ let(:logger) { TestFilterLogger.new }
17
+
18
+ let(:app) {
19
+ log = logger
20
+
21
+ Class.new(ReeRoda::App) do
22
+ plugin(:ree_logger, ree_logger: log, filter_params: ["password", "token"])
23
+
8
24
  route do |r|
9
- r.get "health" do
25
+ r.is "health" do
10
26
  "success"
11
27
  end
12
-
13
- r.ree_routes
14
28
  end
15
29
  end
30
+ }
16
31
 
32
+ def request_line
33
+ logger.logs.first.lines.find { _1.include?("Request:") }
34
+ end
35
+
36
+ def params_line
37
+ logger.logs.first.lines.find { _1.include?("Params:") }
38
+ end
39
+
40
+ it "filters request params" do
17
41
  env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/health", "QUERY_STRING" => "email=test@example.com&password=password" }
18
42
 
19
- class TestFilterLogger
20
- def info(msg)
21
- @logs ||= []
22
- @logs << msg
23
- end
43
+ app.app.call(env)
44
+
45
+ expect(logger.logs.first).to match('"password" => "\[FILTERED\]"')
46
+ end
47
+
48
+ it "filters secrets in the query string of the request line" do
49
+ env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/health", "QUERY_STRING" => "email=test%40example.com&password=s3cret&token=tok-1" }
50
+
51
+ app.app.call(env)
24
52
 
25
- def logs
26
- @logs
53
+ expect(request_line).to include("/health?email=test%40example.com&password=[FILTERED]&token=[FILTERED]")
54
+ expect(logger.logs.first).not_to include("s3cret")
55
+ expect(logger.logs.first).not_to include("tok-1")
56
+ end
57
+
58
+ it "filters bracketed and encoded names in the query string" do
59
+ env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/health", "QUERY_STRING" => "user%5Bpassword%5D=s3cret&user[name]=bob&token_id=7" }
60
+
61
+ app.app.call(env)
62
+
63
+ expect(request_line).to include("user%5Bpassword%5D=[FILTERED]&user[name]=bob&token_id=7")
64
+ expect(logger.logs.first).not_to include("s3cret")
65
+ expect(params_line).to include('"token_id" => "7"')
66
+ end
67
+
68
+ it "keeps an empty query string out of the request line" do
69
+ env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/health", "QUERY_STRING" => "" }
70
+
71
+ app.app.call(env)
72
+
73
+ expect(request_line).to include("Request: GET /health \n")
74
+ end
75
+
76
+ it "filters nested params of a JSON body" do
77
+ body = '{"user":{"name":"bob","password":"s3cret"},"sessions":[{"token":"tok-1","id":1}]}'
78
+ env = {
79
+ "REQUEST_METHOD" => "POST", "PATH_INFO" => "/health",
80
+ "CONTENT_TYPE" => "application/json", "rack.input" => StringIO.new(body)
81
+ }
82
+
83
+ app.app.call(env)
84
+
85
+ expect(params_line).to include('"name" => "bob"')
86
+ expect(params_line).to include('"id" => 1')
87
+ expect(logger.logs.first).not_to include("s3cret")
88
+ expect(logger.logs.first).not_to include("tok-1")
89
+ end
90
+
91
+ it "logs the query string untouched without filter params" do
92
+ log = logger
93
+ plain_app = Class.new(ReeRoda::App) do
94
+ plugin(:ree_logger, ree_logger: log)
95
+
96
+ route do |r|
97
+ r.is "health" do
98
+ "success"
99
+ end
27
100
  end
28
101
  end
29
- logger = TestFilterLogger.new
30
102
 
31
- TestFilterApp.plugin(:ree_logger, ree_logger: logger, filter_params: ['password'])
32
- TestFilterApp.app.call(env)
103
+ env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/health", "QUERY_STRING" => "password=p" }
33
104
 
34
- expect(logger.logs.first).to match('"password" => "\[FILTERED\]"')
105
+ plain_app.app.call(env)
106
+
107
+ expect(request_line).to include("/health?password=p")
35
108
  end
36
- end
109
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReeLib
4
- VERSION = "1.3.15"
4
+ VERSION = "1.3.16"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ree_lib
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.15
4
+ version: 1.3.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ruslan Gatiyatov