Skip to content

Commit 4273771

Browse files
committed
FEATURE: perform security checks earlier in the pipeline
This avoids needing to discard results and ensures no areas of mini profiler can execute prior to a request being authorized authorization token lasts for 30 minutes and is cycled after that
1 parent bf33bd7 commit 4273771

31 files changed

+435
-94
lines changed

Gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ source 'http://rubygems.org'
22

33
gemspec
44

5-
gem 'codecov', :require => false, :group => :test
5+
gem 'codecov', :require => false, :group => :test

Guardfile

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# A sample Guardfile
2+
# More info at https://github.com/guard/guard#readme
3+
4+
directories %w(lib spec) \
5+
.select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
6+
7+
## Note: if you are using the `directories` clause above and you are not
8+
## watching the project directory ('.'), then you will want to move
9+
## the Guardfile to a watched dir and symlink it back, e.g.
10+
#
11+
# $ mkdir config
12+
# $ mv Guardfile config/
13+
# $ ln -s config/Guardfile .
14+
#
15+
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"
16+
17+
# Note: The cmd option is now required due to the increasing number of ways
18+
# rspec may be run, below are examples of the most common uses.
19+
# * bundler: 'bundle exec rspec'
20+
# * bundler binstubs: 'bin/rspec'
21+
# * spring: 'bin/rsspec' (This will use spring if running and you have
22+
# installed the spring binstubs per the docs)
23+
# * zeus: 'zeus rspec' (requires the server to be started separetly)
24+
# * 'just' rspec: 'rspec'
25+
guard :rspec, cmd: 'bundle exec rspec', failed_mode: :focus do
26+
watch(%r{^spec/.+_spec\.rb$})
27+
watch(%r{^lib/mini_profiler/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
28+
watch('spec/spec_helper.rb') { "spec" }
29+
end
30+

autotest/discover.rb

Lines changed: 0 additions & 2 deletions
This file was deleted.

lib/mini_profiler/client_settings.rb

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,39 +12,90 @@ class ClientSettings
1212
attr_accessor :backtrace_level
1313

1414

15-
def initialize(env)
15+
def initialize(env, store, start)
1616
request = ::Rack::Request.new(env)
1717
@cookie = request.cookies[COOKIE_NAME]
18+
@store = store
19+
@start = start
20+
21+
@allowed_tokens, @orig_auth_tokens = nil
22+
1823
if @cookie
1924
@cookie.split(",").map{|pair| pair.split("=")}.each do |k,v|
2025
@orig_disable_profiling = @disable_profiling = (v=='t') if k == "dp"
2126
@backtrace_level = v.to_i if k == "bt"
27+
@orig_auth_tokens = v.to_s.split("|") if k == "a"
2228
end
2329
end
2430

25-
@backtrace_level = nil if !@backtrace_level.nil? && (@backtrace_level == 0 || @backtrace_level > BACKTRACE_NONE)
31+
if !@backtrace_level.nil? && (@backtrace_level == 0 || @backtrace_level > BACKTRACE_NONE)
32+
@backtrace_level = nil
33+
end
34+
2635
@orig_backtrace_level = @backtrace_level
2736

2837
end
2938

39+
def handle_cookie(result)
40+
status,headers,_body = result
41+
42+
if (MiniProfiler.config.authorization_mode == :whitelist && !MiniProfiler.request_authorized?)
43+
# this is non-obvious, don't kill the profiling cookie on errors or short requests
44+
# this ensures that stuff that never reaches the rails stack does not kill profiling
45+
if status.to_i >= 200 && status.to_i < 300 && ((Time.now - @start) > 0.1)
46+
discard_cookie!(headers)
47+
end
48+
else
49+
write!(headers)
50+
end
51+
52+
result
53+
end
54+
3055
def write!(headers)
31-
if @orig_disable_profiling != @disable_profiling || @orig_backtrace_level != @backtrace_level || @cookie.nil?
56+
57+
tokens_changed = false
58+
59+
if MiniProfiler.request_authorized? && MiniProfiler.config.authorization_mode == :whitelist
60+
@allowed_tokens ||= @store.allowed_tokens
61+
tokens_changed = !@orig_auth_tokens || ((@allowed_tokens - @orig_auth_tokens).length > 0)
62+
end
63+
64+
if @orig_disable_profiling != @disable_profiling ||
65+
@orig_backtrace_level != @backtrace_level ||
66+
@cookie.nil? ||
67+
tokens_changed
68+
3269
settings = {"p" => "t" }
33-
settings["dp"] = "t" if @disable_profiling
34-
settings["bt"] = @backtrace_level if @backtrace_level
70+
settings["dp"] = "t" if @disable_profiling
71+
settings["bt"] = @backtrace_level if @backtrace_level
72+
settings["a"] = @allowed_tokens.join("|") if @allowed_tokens && MiniProfiler.request_authorized?
73+
3574
settings_string = settings.map{|k,v| "#{k}=#{v}"}.join(",")
3675
Rack::Utils.set_cookie_header!(headers, COOKIE_NAME, :value => settings_string, :path => '/')
3776
end
3877
end
3978

4079
def discard_cookie!(headers)
41-
Rack::Utils.delete_cookie_header!(headers, COOKIE_NAME, :path => '/')
80+
if @cookie
81+
Rack::Utils.delete_cookie_header!(headers, COOKIE_NAME, :path => '/')
82+
end
4283
end
4384

44-
def has_cookie?
45-
!@cookie.nil?
85+
def has_valid_cookie?
86+
valid_cookie = !@cookie.nil?
87+
88+
if (MiniProfiler.config.authorization_mode == :whitelist)
89+
@allowed_tokens ||= @store.allowed_tokens
90+
91+
valid_cookie = (Array === @orig_auth_tokens) &&
92+
((@allowed_tokens & @orig_auth_tokens).length > 0)
93+
end
94+
95+
valid_cookie
4696
end
4797

98+
4899
def disable_profiling?
49100
@disable_profiling
50101
end

lib/mini_profiler/context.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
class Rack::MiniProfiler::Context
2-
attr_accessor :inject_js,:current_timer,:page_struct,:skip_backtrace,:full_backtrace,:discard, :mpt_init, :measure
2+
attr_accessor :inject_js,:current_timer,:page_struct,:skip_backtrace,
3+
:full_backtrace,:discard, :mpt_init, :measure
34

45
def initialize(opts = {})
56
opts["measure"] = true unless opts.key? "measure"

lib/mini_profiler/profiler.rb

Lines changed: 21 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,9 @@ def config
149149

150150
def call(env)
151151

152-
client_settings = ClientSettings.new(env)
152+
start = Time.now
153+
client_settings = ClientSettings.new(env, @storage, start)
154+
MiniProfiler.deauthorize_request if @config.authorization_mode == :whitelist
153155

154156
status = headers = body = nil
155157
query_string = env['QUERY_STRING']
@@ -162,43 +164,39 @@ def call(env)
162164
(@config.skip_paths && @config.skip_paths.any?{ |p| path.start_with?(p) }) ||
163165
query_string =~ /pp=skip/
164166

165-
has_profiling_cookie = client_settings.has_cookie?
166-
167-
if skip_it || (@config.authorization_mode == :whitelist && !has_profiling_cookie)
168-
status,headers,body = @app.call(env)
169-
if !skip_it && @config.authorization_mode == :whitelist && !has_profiling_cookie && MiniProfiler.request_authorized?
170-
client_settings.write!(headers)
171-
end
172-
return [status,headers,body]
167+
if skip_it || (
168+
@config.authorization_mode == :whitelist &&
169+
!client_settings.has_valid_cookie?
170+
)
171+
return client_settings.handle_cookie(@app.call(env))
173172
end
174173

175174
# handle all /mini-profiler requests here
176-
return serve_html(env) if path.start_with? @config.base_url_path
175+
return client_settings.handle_cookie(serve_html(env)) if path.start_with? @config.base_url_path
177176

178177
has_disable_cookie = client_settings.disable_profiling?
179178
# manual session disable / enable
180179
if query_string =~ /pp=disable/ || has_disable_cookie
181180
skip_it = true
182181
end
183182

184-
if query_string =~ /pp=enable/ && (@config.authorization_mode != :whitelist || MiniProfiler.request_authorized?)
183+
if query_string =~ /pp=enable/
185184
skip_it = false
186185
config.enabled = true
187186
end
188187

189188
if skip_it || !config.enabled
190189
status,headers,body = @app.call(env)
191190
client_settings.disable_profiling = true
192-
client_settings.write!(headers)
193-
return [status,headers,body]
191+
return client_settings.handle_cookie([status,headers,body])
194192
else
195193
client_settings.disable_profiling = false
196194
end
197195

198196
# profile gc
199197
if query_string =~ /pp=profile-gc/
200198
current.measure = false if current
201-
return Rack::MiniProfiler::GCProfiler.new.profile_gc(@app, env)
199+
return client_settings.handle_cookie(Rack::MiniProfiler::GCProfiler.new.profile_gc(@app, env))
202200
end
203201

204202
# profile memory
@@ -215,11 +213,10 @@ def call(env)
215213
body.close if body.respond_to? :close
216214
end
217215
report.pretty_print(result)
218-
return text_result(result.string)
216+
return client_settings.handle_cookie(text_result(result.string))
219217
end
220218

221219
MiniProfiler.create_current(env, @config)
222-
MiniProfiler.deauthorize_request if @config.authorization_mode == :whitelist
223220

224221
if query_string =~ /pp=normal-backtrace/
225222
client_settings.backtrace_level = ClientSettings::BACKTRACE_DEFAULT
@@ -238,7 +235,6 @@ def call(env)
238235
trace_exceptions = query_string =~ /pp=trace-exceptions/ && defined? TracePoint
239236
status, headers, body, exceptions,trace = nil
240237

241-
start = Time.now
242238

243239
if trace_exceptions
244240
exceptions = []
@@ -281,43 +277,37 @@ def call(env)
281277
else
282278
status,headers,body = @app.call(env)
283279
end
284-
client_settings.write!(headers)
285280
ensure
286281
trace.disable if trace
287282
end
288283

289284
skip_it = current.discard
290285

291286
if (config.authorization_mode == :whitelist && !MiniProfiler.request_authorized?)
292-
# this is non-obvious, don't kill the profiling cookie on errors or short requests
293-
# this ensures that stuff that never reaches the rails stack does not kill profiling
294-
if status.to_i >= 200 && status.to_i < 300 && ((Time.now - start) > 0.1)
295-
client_settings.discard_cookie!(headers)
296-
end
297287
skip_it = true
298288
end
299289

300-
return [status,headers,body] if skip_it
290+
return client_settings.handle_cookie([status,headers,body]) if skip_it
301291

302292
# we must do this here, otherwise current[:discard] is not being properly treated
303293
if trace_exceptions
304294
body.close if body.respond_to? :close
305-
return dump_exceptions exceptions
295+
return client_settings.handle_cookie(dump_exceptions exceptions)
306296
end
307297

308298
if query_string =~ /pp=env/ && !config.disable_env_dump
309299
body.close if body.respond_to? :close
310-
return dump_env env
300+
return client_settings.handle_cookie(dump_env env)
311301
end
312302

313303
if query_string =~ /pp=analyze-memory/
314304
body.close if body.respond_to? :close
315-
return analyze_memory
305+
return client_settings.handle_cookie(analyze_memory)
316306
end
317307

318308
if query_string =~ /pp=help/
319309
body.close if body.respond_to? :close
320-
return help(client_settings, env)
310+
return client_settings.handle_cookie(help(client_settings, env))
321311
end
322312

323313
page_struct = current.page_struct
@@ -326,7 +316,7 @@ def call(env)
326316

327317
if flamegraph
328318
body.close if body.respond_to? :close
329-
return self.flamegraph(flamegraph)
319+
return client_settings.handle_cookie(self.flamegraph(flamegraph))
330320
end
331321

332322

@@ -337,18 +327,16 @@ def call(env)
337327

338328
# inject headers, script
339329
if status >= 200 && status < 300
340-
client_settings.write!(headers)
341330
result = inject_profiler(env,status,headers,body)
342-
return result if result
331+
return client_settings.handle_cookie(result) if result
343332
end
344333
rescue Exception => e
345334
if @config.storage_failure != nil
346335
@config.storage_failure.call(e)
347336
end
348337
end
349338

350-
client_settings.write!(headers)
351-
[status, headers, body]
339+
client_settings.handle_cookie([status, headers, body])
352340

353341
ensure
354342
# Make sure this always happens
@@ -543,7 +531,6 @@ def help(client_settings, env)
543531
</html>
544532
"
545533

546-
client_settings.write!(headers)
547534
[200, headers, [body]]
548535
end
549536

lib/mini_profiler/storage/abstract_store.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ module Rack
22
class MiniProfiler
33
class AbstractStore
44

5+
# maximum age of allowed tokens before cycling in seconds
6+
MAX_TOKEN_AGE = 1800
7+
58
def save(page_struct)
69
raise NotImplementedError.new("save is not implemented")
710
end
@@ -31,6 +34,11 @@ def diagnostics(user)
3134
""
3235
end
3336

37+
# a list of tokens that are permitted to access profiler in whitelist mode
38+
def allowed_tokens
39+
raise NotImplementedError.new("allowed_tokens is not implemented")
40+
end
41+
3442
end
3543
end
3644
end

lib/mini_profiler/storage/file_store.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ def initialize(args = nil)
5151
@user_view_cache = FileCache.new(@path, "mp_views")
5252
@user_view_lock = Mutex.new
5353

54+
@auth_token_cache = FileCache.new(@path, "tokens")
55+
@auth_token_lock = Mutex.new
56+
5457
me = self
5558
t = CacheCleanupThread.new do
5659
interval = 10
@@ -126,6 +129,28 @@ def get_unviewed_ids(user)
126129
}
127130
end
128131

132+
def flush_tokens
133+
@auth_token_lock.synchronize {
134+
@auth_token_cache[""] = nil
135+
}
136+
end
137+
138+
def allowed_tokens
139+
@auth_token_lock.synchronize {
140+
token1, token2, cycle_at = @auth_token_cache[""]
141+
142+
unless cycle_at && (Time === cycle_at) && (cycle_at > Time.now)
143+
token2 = token1
144+
token1 = SecureRandom.hex
145+
cycle_at = Time.now + Rack::MiniProfiler::AbstractStore::MAX_TOKEN_AGE
146+
end
147+
148+
@auth_token_cache[""] = [token1, token2, cycle_at]
149+
150+
[token1, token2].compact
151+
}
152+
end
153+
129154
def cleanup_cache
130155
files = Dir.entries(@path)
131156
@timer_struct_lock.synchronize {

0 commit comments

Comments
 (0)