rails-profiler 0.30.5 → 0.30.7
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/CHANGELOG.md +80 -0
- data/app/assets/builds/profiler-toolbar.js +2 -1
- data/app/assets/builds/profiler.js +17 -3
- data/app/controllers/profiler/api/ajax_controller.rb +0 -2
- data/app/controllers/profiler/api/cluster_controller.rb +0 -2
- data/app/controllers/profiler/api/console_controller.rb +0 -2
- data/app/controllers/profiler/api/env_vars_controller.rb +22 -7
- data/app/controllers/profiler/api/events_controller.rb +0 -1
- data/app/controllers/profiler/api/explain_controller.rb +0 -2
- data/app/controllers/profiler/api/function_profiling_controller.rb +0 -2
- data/app/controllers/profiler/api/jobs_controller.rb +0 -2
- data/app/controllers/profiler/api/outbound_http_controller.rb +0 -2
- data/app/controllers/profiler/api/profiles_controller.rb +0 -2
- data/app/controllers/profiler/api/slave_proxy_controller.rb +0 -2
- data/app/controllers/profiler/api/test_runner_controller.rb +0 -2
- data/app/controllers/profiler/api/tests_controller.rb +0 -2
- data/app/controllers/profiler/api/toolbar_controller.rb +0 -1
- data/app/controllers/profiler/application_controller.rb +33 -2
- data/app/controllers/profiler/assets_controller.rb +3 -0
- data/app/controllers/profiler/profiles_controller.rb +0 -9
- data/lib/profiler/cluster/master_client.rb +4 -2
- data/lib/profiler/cluster/slave_proxy.rb +1 -0
- data/lib/profiler/collectors/database_collector.rb +5 -1
- data/lib/profiler/collectors/env_collector.rb +1 -1
- data/lib/profiler/collectors/mailer_collector.rb +13 -1
- data/lib/profiler/collectors/request_collector.rb +1 -1
- data/lib/profiler/configuration.rb +42 -4
- data/lib/profiler/explain_runner.rb +4 -0
- data/lib/profiler/instrumentation/net_http_instrumentation.rb +6 -4
- data/lib/profiler/job_profiler.rb +2 -1
- data/lib/profiler/local_request.rb +163 -0
- data/lib/profiler/mcp/tools/list_env_vars.rb +4 -3
- data/lib/profiler/mcp/tools/reset_env_var.rb +2 -1
- data/lib/profiler/mcp/tools/set_env_var.rb +5 -0
- data/lib/profiler/middleware/cors_middleware.rb +47 -35
- data/lib/profiler/models/profile.rb +9 -3
- data/lib/profiler/railtie.rb +4 -5
- data/lib/profiler/redaction.rb +421 -0
- data/lib/profiler/version.rb +1 -1
- data/lib/profiler.rb +1 -0
- metadata +4 -2
|
@@ -5,6 +5,7 @@ require "base64"
|
|
|
5
5
|
require "zlib"
|
|
6
6
|
require "stringio"
|
|
7
7
|
require "securerandom"
|
|
8
|
+
require_relative "../redaction"
|
|
8
9
|
|
|
9
10
|
module Profiler
|
|
10
11
|
module Instrumentation
|
|
@@ -20,7 +21,7 @@ module Profiler
|
|
|
20
21
|
host = address.to_s
|
|
21
22
|
return super if NetHttpInstrumentation.skip_host?(host)
|
|
22
23
|
|
|
23
|
-
url = build_url(host, port, req.path, use_ssl?)
|
|
24
|
+
url = Redaction.filter_url(build_url(host, port, req.path, use_ssl?))
|
|
24
25
|
req_body = req.body.to_s
|
|
25
26
|
# Fallback: body may be passed as the 2nd argument and only applied
|
|
26
27
|
# to req inside super via req.set_body_internal(body)
|
|
@@ -43,7 +44,7 @@ module Profiler
|
|
|
43
44
|
req_body = ""
|
|
44
45
|
end
|
|
45
46
|
end
|
|
46
|
-
req_headers = req.to_hash.transform_values { |v| v.join(", ") }
|
|
47
|
+
req_headers = Redaction.filter_headers(req.to_hash.transform_values { |v| v.join(", ") })
|
|
47
48
|
req_content_type = req["content-type"].to_s
|
|
48
49
|
processed_req = req_body.empty? ? { body: nil, encoding: "text" } : NetHttpInstrumentation.process_body(req_body, req_content_type)
|
|
49
50
|
|
|
@@ -83,7 +84,7 @@ module Profiler
|
|
|
83
84
|
entry,
|
|
84
85
|
status: response.code.to_i,
|
|
85
86
|
duration: duration,
|
|
86
|
-
response_headers: response.to_hash.transform_values { |v| v.join(", ") },
|
|
87
|
+
response_headers: Redaction.filter_headers(response.to_hash.transform_values { |v| v.join(", ") }),
|
|
87
88
|
response_body: processed_resp[:body],
|
|
88
89
|
response_body_encoding: processed_resp[:encoding],
|
|
89
90
|
response_size: resp_body_raw.bytesize
|
|
@@ -151,7 +152,8 @@ module Profiler
|
|
|
151
152
|
if mime.match?(BINARY_CONTENT_TYPES)
|
|
152
153
|
{ body: Base64.strict_encode64(body.b), encoding: "base64" }
|
|
153
154
|
else
|
|
154
|
-
text =
|
|
155
|
+
text = Redaction.filter_body(body, content_type)
|
|
156
|
+
.encode("UTF-8", invalid: :replace, undef: :replace, replace: "?")
|
|
155
157
|
{ body: text, encoding: "text" }
|
|
156
158
|
end
|
|
157
159
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "models/profile"
|
|
4
4
|
require_relative "current_context"
|
|
5
|
+
require_relative "redaction"
|
|
5
6
|
require_relative "collectors/job_collector"
|
|
6
7
|
require_relative "collectors/database_collector"
|
|
7
8
|
require_relative "collectors/cache_collector"
|
|
@@ -115,7 +116,7 @@ module Profiler
|
|
|
115
116
|
def sanitize_arguments(args)
|
|
116
117
|
return [] unless args
|
|
117
118
|
|
|
118
|
-
args.map do |arg|
|
|
119
|
+
Redaction.filter_value(args).map do |arg|
|
|
119
120
|
case arg
|
|
120
121
|
when String then arg.length > 200 ? "#{arg[0, 200]}..." : arg
|
|
121
122
|
when Numeric, TrueClass, FalseClass, NilClass then arg
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
|
|
5
|
+
module Profiler
|
|
6
|
+
# Decides whether a request comes from this machine, for authorization_mode :allow_local.
|
|
7
|
+
#
|
|
8
|
+
# The client address is read from REMOTE_ADDR only. Forwarding headers (X-Forwarded-For,
|
|
9
|
+
# X-Real-IP, Forwarded, X-Forwarded-Host) can be forged, so they never grant access: they
|
|
10
|
+
# can only refuse it, when a local reverse proxy reports a remote client. The Host header
|
|
11
|
+
# has to be a local name, or one the application allows in config.hosts, which defeats DNS
|
|
12
|
+
# rebinding: a rebound page reaches 127.0.0.1 under the attacker's own domain name. The
|
|
13
|
+
# test environment also accepts the default hosts of rack-test and of Rails integration
|
|
14
|
+
# tests, reserved names (RFC 2606) that no attacker can point at a server of his own.
|
|
15
|
+
module LocalRequest
|
|
16
|
+
FORWARDING_HEADERS = {
|
|
17
|
+
"HTTP_X_FORWARDED_FOR" => "X-Forwarded-For",
|
|
18
|
+
"HTTP_X_REAL_IP" => "X-Real-IP",
|
|
19
|
+
"HTTP_FORWARDED" => "Forwarded"
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
TEST_HOSTS = %w[www.example.com example.com example.org].freeze
|
|
23
|
+
|
|
24
|
+
@warned = false
|
|
25
|
+
@warn_mutex = Mutex.new
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
# Returns nil when the request is local, otherwise a sentence naming the cause.
|
|
29
|
+
def denial_reason(request)
|
|
30
|
+
remote_addr = request.get_header("REMOTE_ADDR").to_s
|
|
31
|
+
unless loopback_address?(remote_addr)
|
|
32
|
+
return "REMOTE_ADDR #{remote_addr.empty? ? "(missing)" : remote_addr} is not a loopback address"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
FORWARDING_HEADERS.each do |key, name|
|
|
36
|
+
value = request.get_header(key)
|
|
37
|
+
next if value.nil? || value.empty?
|
|
38
|
+
|
|
39
|
+
nodes = key == "HTTP_FORWARDED" ? forwarded_param(value, "for") : value.split(",")
|
|
40
|
+
remote = nodes.map { |node| strip_port(node) }.find { |address| !loopback_address?(address) }
|
|
41
|
+
return "the #{name} header reports a non-local client (#{remote})" if remote
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
host_denial_reason(request)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def local?(request)
|
|
48
|
+
denial_reason(request).nil?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Logs why a request was refused, once per process, so that an application running in
|
|
52
|
+
# Docker does not just stop being profiled with no explanation.
|
|
53
|
+
def warn_once(reason)
|
|
54
|
+
@warn_mutex.synchronize do
|
|
55
|
+
return if @warned
|
|
56
|
+
|
|
57
|
+
@warned = true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
message = "[Profiler] Request refused or not profiled by authorization_mode :allow_local: #{reason}. " \
|
|
61
|
+
"The profiler only serves and captures requests made from this machine. " \
|
|
62
|
+
"If the application runs in Docker or behind a remote proxy, either set " \
|
|
63
|
+
"config.authorization_mode = :allow_authorized with a config.authorize_with block " \
|
|
64
|
+
"that admits your network (see the Access control section of the README), or set " \
|
|
65
|
+
"config.authorization_mode = :allow_all, which offers no protection at all. " \
|
|
66
|
+
"This message is logged once per process."
|
|
67
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
68
|
+
Rails.logger.warn(message)
|
|
69
|
+
else
|
|
70
|
+
warn(message)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def reset_warning!
|
|
75
|
+
@warn_mutex.synchronize { @warned = false }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def loopback_address?(value)
|
|
79
|
+
address = value.to_s.strip
|
|
80
|
+
address = address[1..-2] if address.start_with?("[") && address.end_with?("]")
|
|
81
|
+
# Only an IPv6 address carries a zone identifier ("fe80::1%eth0").
|
|
82
|
+
address = address.sub(/%.*\z/, "") if address.include?(":")
|
|
83
|
+
return false if address.empty?
|
|
84
|
+
|
|
85
|
+
ip = IPAddr.new(address)
|
|
86
|
+
ip = ip.native if ip.ipv6? && ip.ipv4_mapped?
|
|
87
|
+
ip.loopback?
|
|
88
|
+
rescue IPAddr::Error
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def host_denial_reason(request)
|
|
95
|
+
# A request with no Host header does not come from a browser, so it cannot be a
|
|
96
|
+
# rebound page; REMOTE_ADDR alone decides.
|
|
97
|
+
raw_host = request.get_header("HTTP_HOST").to_s
|
|
98
|
+
unless raw_host.empty?
|
|
99
|
+
host = host_name(raw_host)
|
|
100
|
+
return "the Host header #{host.inspect} is not a local name" unless allowed_host?(host)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
forwarded_hosts = request.get_header("HTTP_X_FORWARDED_HOST").to_s.split(",")
|
|
104
|
+
remote = forwarded_hosts.map { |h| host_name(h) }.find { |h| !allowed_host?(h) }
|
|
105
|
+
return "the X-Forwarded-Host header #{remote.inspect} is not a local name" if remote
|
|
106
|
+
|
|
107
|
+
forwarded = request.get_header("HTTP_FORWARDED").to_s
|
|
108
|
+
remote = forwarded_param(forwarded, "host").map { |h| host_name(h) }.find { |h| !allowed_host?(h) }
|
|
109
|
+
return "the Forwarded header names a non-local host (#{remote.inspect})" if remote
|
|
110
|
+
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def test_environment?
|
|
115
|
+
defined?(Rails) && Rails.respond_to?(:env) && Rails.env.test?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Values of one parameter (for=, host=) of a Forwarded header (RFC 7239), quotes removed.
|
|
119
|
+
def forwarded_param(value, param)
|
|
120
|
+
value.split(/[,;]/).filter_map do |pair|
|
|
121
|
+
name, node = pair.split("=", 2)
|
|
122
|
+
next unless name.to_s.strip.casecmp?(param)
|
|
123
|
+
|
|
124
|
+
node.to_s.strip.delete_prefix('"').delete_suffix('"')
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# "127.0.0.1:5555" -> "127.0.0.1", "[::1]:5555" -> "::1", "localhost:3000" -> "localhost".
|
|
129
|
+
# Any other form, a bare IPv6 address included, is returned as is: a malformed value is
|
|
130
|
+
# then refused unless it is local by itself.
|
|
131
|
+
def strip_port(value)
|
|
132
|
+
value = value.to_s.strip
|
|
133
|
+
match = value.match(/\A\[([^\]]+)\](?::\d+)?\z/) || value.match(/\A([^:]+)(?::\d+)?\z/)
|
|
134
|
+
match ? match[1] : value
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def host_name(value)
|
|
138
|
+
strip_port(value).downcase.chomp(".")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def allowed_host?(host)
|
|
142
|
+
return false if host.empty?
|
|
143
|
+
return true if host == "localhost" || host.end_with?(".localhost")
|
|
144
|
+
return true if loopback_address?(host)
|
|
145
|
+
return true if test_environment? && TEST_HOSTS.include?(host)
|
|
146
|
+
|
|
147
|
+
permitted_by_rails_hosts?(host)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# When the application restricts config.hosts, ActionDispatch::HostAuthorization has
|
|
151
|
+
# already refused every other host; a host it lists is one the developer trusts.
|
|
152
|
+
def permitted_by_rails_hosts?(host)
|
|
153
|
+
return false unless defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
|
154
|
+
return false unless defined?(ActionDispatch::HostAuthorization::Permissions)
|
|
155
|
+
|
|
156
|
+
hosts = Rails.application.config.hosts
|
|
157
|
+
return false if hosts.nil? || hosts.empty?
|
|
158
|
+
|
|
159
|
+
ActionDispatch::HostAuthorization::Permissions.new(hosts).allows?(host)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../slave_support"
|
|
4
4
|
require_relative "../../env_override_store"
|
|
5
|
+
require_relative "../../redaction"
|
|
5
6
|
|
|
6
7
|
module Profiler
|
|
7
8
|
module MCP
|
|
@@ -19,8 +20,8 @@ module Profiler
|
|
|
19
20
|
filter = params["filter"]&.downcase
|
|
20
21
|
|
|
21
22
|
if include_all
|
|
22
|
-
vars =
|
|
23
|
-
overrides = Profiler.env_override_store.all_overrides
|
|
23
|
+
vars = Profiler::Redaction.env_snapshot
|
|
24
|
+
overrides = Profiler::Redaction.env_overrides(Profiler.env_override_store.all_overrides)
|
|
24
25
|
|
|
25
26
|
vars = vars.select { |k, _| k.downcase.include?(filter) } if filter
|
|
26
27
|
|
|
@@ -39,7 +40,7 @@ module Profiler
|
|
|
39
40
|
text += "|-----|--------------|------------|\n"
|
|
40
41
|
text += rows.join("\n")
|
|
41
42
|
else
|
|
42
|
-
overrides = Profiler.env_override_store.all_overrides
|
|
43
|
+
overrides = Profiler::Redaction.env_overrides(Profiler.env_override_store.all_overrides)
|
|
43
44
|
overrides = overrides.select { |k, _| k.downcase.include?(filter) } if filter
|
|
44
45
|
|
|
45
46
|
if overrides.empty?
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../slave_support"
|
|
4
|
+
require_relative "../../redaction"
|
|
4
5
|
|
|
5
6
|
require "uri"
|
|
6
7
|
|
|
@@ -26,7 +27,7 @@ module Profiler
|
|
|
26
27
|
original = overrides[key]["original"]
|
|
27
28
|
Profiler.env_override_store.reset(key)
|
|
28
29
|
|
|
29
|
-
restored = original
|
|
30
|
+
restored = original.nil? ? "(unset)" : Profiler::Redaction.env_value(key, original)
|
|
30
31
|
[{ type: "text", text: "Reset #{key} to original value: #{restored}" }]
|
|
31
32
|
end
|
|
32
33
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../slave_support"
|
|
4
|
+
require_relative "../../redaction"
|
|
4
5
|
|
|
5
6
|
module Profiler
|
|
6
7
|
module MCP
|
|
@@ -11,6 +12,10 @@ module Profiler
|
|
|
11
12
|
value = params["value"].to_s
|
|
12
13
|
|
|
13
14
|
return [{ type: "text", text: "Error: key cannot be blank." }] if key.empty?
|
|
15
|
+
if Profiler::Redaction.mask?(value)
|
|
16
|
+
return [{ type: "text", text: "Error: #{Profiler::Redaction::MASK} is the mask the profiler shows " \
|
|
17
|
+
"in place of a hidden value, not a value; #{key} was left unchanged." }]
|
|
18
|
+
end
|
|
14
19
|
|
|
15
20
|
if (proxy = MCP::SlaveSupport.with_slave_proxy(params))
|
|
16
21
|
proxy.patch_json("/_profiler/api/env_vars", { key: key, value: value })
|
|
@@ -2,60 +2,62 @@
|
|
|
2
2
|
|
|
3
3
|
module Profiler
|
|
4
4
|
module Middleware
|
|
5
|
+
# Sets the framing headers of every profiler response, and its CORS headers when
|
|
6
|
+
# extension_cors_enabled is set. Both settings are read on each request.
|
|
5
7
|
class CorsMiddleware
|
|
6
8
|
def initialize(app)
|
|
7
9
|
@app = app
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
def call(env)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
['']
|
|
19
|
-
]
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
status, headers, body = @app.call(env)
|
|
23
|
-
headers = headers.dup
|
|
24
|
-
|
|
25
|
-
# Add CORS headers
|
|
26
|
-
cors_headers(env).each do |key, value|
|
|
27
|
-
headers[key] = value
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Allow embedding in iframes from Chrome extensions
|
|
31
|
-
headers.delete('X-Frame-Options')
|
|
32
|
-
# Don't set CSP if controller requested to skip it
|
|
33
|
-
unless env['profiler.skip_csp']
|
|
34
|
-
# Allow embedding from standard web origins
|
|
35
|
-
headers['Content-Security-Policy'] = "frame-ancestors 'self' http: https:"
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
[status, headers, body]
|
|
39
|
-
else
|
|
40
|
-
@app.call(env)
|
|
13
|
+
return @app.call(env) unless profiler_path?(env['PATH_INFO'].to_s)
|
|
14
|
+
|
|
15
|
+
cors = Profiler.configuration.extension_cors_enabled
|
|
16
|
+
|
|
17
|
+
# Handle OPTIONS preflight request
|
|
18
|
+
if cors && env['REQUEST_METHOD'] == 'OPTIONS'
|
|
19
|
+
return [200, cors_headers(env), ['']]
|
|
41
20
|
end
|
|
21
|
+
|
|
22
|
+
status, headers, body = @app.call(env)
|
|
23
|
+
headers = headers.dup
|
|
24
|
+
|
|
25
|
+
cors_headers(env).each { |key, value| set_header(headers, key, value) } if cors
|
|
26
|
+
|
|
27
|
+
# frame-ancestors decides in every browser that knows it, which then ignores
|
|
28
|
+
# X-Frame-Options; SAMEORIGIN only protects older browsers.
|
|
29
|
+
set_header(headers, 'X-Frame-Options', 'SAMEORIGIN')
|
|
30
|
+
set_header(headers, 'Content-Security-Policy', "frame-ancestors #{frame_ancestors}")
|
|
31
|
+
|
|
32
|
+
[status, headers, body]
|
|
42
33
|
end
|
|
43
34
|
|
|
44
35
|
private
|
|
45
36
|
|
|
37
|
+
# "/_profiler" itself is the dashboard URL; "/_profilerfoo" belongs to the application.
|
|
38
|
+
def profiler_path?(path)
|
|
39
|
+
path == '/_profiler' || path.start_with?('/_profiler/')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def frame_ancestors
|
|
43
|
+
sources = Array(Profiler.configuration.frame_ancestors).map(&:to_s).reject(&:empty?)
|
|
44
|
+
sources.empty? ? "'none'" : sources.join(' ')
|
|
45
|
+
end
|
|
46
|
+
|
|
46
47
|
def cors_headers(env)
|
|
47
|
-
allowed_origins = Profiler.configuration.cors_allowed_origins
|
|
48
|
+
allowed_origins = Array(Profiler.configuration.cors_allowed_origins)
|
|
48
49
|
request_origin = env['HTTP_ORIGIN']
|
|
49
50
|
|
|
50
|
-
if allowed_origins.include?(
|
|
51
|
-
origin_header = '*'
|
|
52
|
-
elsif request_origin && allowed_origins.include?(request_origin)
|
|
51
|
+
if request_origin && allowed_origins.include?(request_origin)
|
|
53
52
|
origin_header = request_origin
|
|
53
|
+
elsif allowed_origins.include?('*') && !credentialed?(env)
|
|
54
|
+
# Never on a request carrying credentials: they may be what authorizes it.
|
|
55
|
+
origin_header = '*'
|
|
54
56
|
end
|
|
55
57
|
|
|
56
58
|
headers = {
|
|
57
59
|
'Access-Control-Allow-Methods' => 'GET, POST, OPTIONS',
|
|
58
|
-
'Access-Control-Allow-Headers' =>
|
|
60
|
+
'Access-Control-Allow-Headers' => "Content-Type, X-Requested-With, Accept, #{Profiler::FORGERY_PROTECTION_HEADER}",
|
|
59
61
|
'Access-Control-Expose-Headers' => 'X-Profiler-Token'
|
|
60
62
|
}
|
|
61
63
|
|
|
@@ -66,6 +68,16 @@ module Profiler
|
|
|
66
68
|
|
|
67
69
|
headers
|
|
68
70
|
end
|
|
71
|
+
|
|
72
|
+
def credentialed?(env)
|
|
73
|
+
!env['HTTP_COOKIE'].to_s.empty? || !env['HTTP_AUTHORIZATION'].to_s.empty?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Replaces a header whatever the case of its existing key, plain Hash or Rack::Headers.
|
|
77
|
+
def set_header(headers, name, value)
|
|
78
|
+
headers.keys.each { |key| headers.delete(key) if key.to_s.casecmp?(name) }
|
|
79
|
+
headers[name] = value
|
|
80
|
+
end
|
|
69
81
|
end
|
|
70
82
|
end
|
|
71
83
|
end
|
|
@@ -4,6 +4,7 @@ require "base64"
|
|
|
4
4
|
require "securerandom"
|
|
5
5
|
require "json"
|
|
6
6
|
require "zlib"
|
|
7
|
+
require_relative "../redaction"
|
|
7
8
|
|
|
8
9
|
module Profiler
|
|
9
10
|
module Models
|
|
@@ -46,7 +47,7 @@ module Profiler
|
|
|
46
47
|
@finished_at = Time.now
|
|
47
48
|
@duration = ((@finished_at - @started_at) * 1000).round(2) # milliseconds
|
|
48
49
|
@status = status
|
|
49
|
-
@response_headers = response_headers
|
|
50
|
+
@response_headers = Redaction.filter_headers(response_headers)
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
def add_collector_data(name, data)
|
|
@@ -161,7 +162,7 @@ module Profiler
|
|
|
161
162
|
if binary_content_type?(content_type)
|
|
162
163
|
{ body: Base64.strict_encode64(raw.b), encoding: "base64" }
|
|
163
164
|
else
|
|
164
|
-
text = raw.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
165
|
+
text = Redaction.filter_body(raw, content_type).encode("UTF-8", invalid: :replace, undef: :replace)
|
|
165
166
|
if compress_body?(text)
|
|
166
167
|
{ body: Base64.strict_encode64(Zlib::Deflate.deflate(text)), encoding: "gzip+base64" }
|
|
167
168
|
else
|
|
@@ -189,10 +190,14 @@ module Profiler
|
|
|
189
190
|
ct.to_s.match?(%r{image/(?!svg)|application/(?:pdf|octet-stream|zip)|audio/|video/})
|
|
190
191
|
end
|
|
191
192
|
|
|
193
|
+
LEGACY_FILTERED_PARAMS = %w[password password_confirmation token secret].freeze
|
|
194
|
+
|
|
192
195
|
def sanitize_params(params)
|
|
193
196
|
return {} unless params
|
|
194
197
|
|
|
195
|
-
params.to_h.except(
|
|
198
|
+
return params.to_h.except(*LEGACY_FILTERED_PARAMS) unless Redaction.enabled?
|
|
199
|
+
|
|
200
|
+
Redaction.filter_hash(params.to_h)
|
|
196
201
|
end
|
|
197
202
|
|
|
198
203
|
ALLOWED_HEADERS = %w[
|
|
@@ -206,6 +211,7 @@ module Profiler
|
|
|
206
211
|
env.select { |k, _| k.start_with?("HTTP_") }
|
|
207
212
|
.transform_keys { |k| k.sub(/^HTTP_/, "").split("_").map(&:capitalize).join("-") }
|
|
208
213
|
.select { |k, _| ALLOWED_HEADERS.include?(k) }
|
|
214
|
+
.then { |headers| Redaction.filter_headers(headers) }
|
|
209
215
|
end
|
|
210
216
|
end
|
|
211
217
|
end
|
data/lib/profiler/railtie.rb
CHANGED
|
@@ -24,11 +24,10 @@ module Profiler
|
|
|
24
24
|
if Profiler.configuration.enabled
|
|
25
25
|
require_relative "middleware/profiler_middleware"
|
|
26
26
|
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
end
|
|
27
|
+
# Always inserted: it sets the framing headers of every profiler response, and its
|
|
28
|
+
# CORS headers only when extension_cors_enabled is set.
|
|
29
|
+
require_relative "middleware/cors_middleware"
|
|
30
|
+
app.middleware.insert_before 0, Profiler::Middleware::CorsMiddleware
|
|
32
31
|
|
|
33
32
|
app.middleware.insert_before 0, Profiler::Middleware::ProfilerMiddleware
|
|
34
33
|
end
|