Skip to content

Commit 4f4e29b

Browse files
committed
Fix CSP nonce support for flamegraph rendering
The flamegraph page was missing CSP nonce attributes on its inline script tags, causing Content Security Policy violations when nonces were configured. This change extracts the nonce retrieval logic into a shared method and applies it consistently to both the main profiler UI and flamegraph pages.
1 parent e1905f1 commit 4f4e29b

3 files changed

Lines changed: 84 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG
22

3+
## Unreleased
4+
5+
- [FIX] Add CSP nonce support to flamegraph rendering [#](https://github.com/MiniProfiler/rack-mini-profiler/pull/)
6+
37
## 4.0 - 2025-06-11
48

59
- [BREAKING CHANGE] Ruby version 3.1.0 or later is required. [#632](https://github.com/MiniProfiler/rack-mini-profiler/pull/632)

lib/mini_profiler/views.rb

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ def share_template
1010
@share_template ||= ERB.new(::File.read(::File.expand_path("../html/share.html", ::File.dirname(__FILE__))))
1111
end
1212

13+
def get_csp_nonce(env, response_headers = {})
14+
configured_nonce = @config.content_security_policy_nonce
15+
if configured_nonce && !configured_nonce.is_a?(String)
16+
configured_nonce = configured_nonce.call(env, response_headers)
17+
end
18+
19+
configured_nonce ||
20+
env["action_dispatch.content_security_policy_nonce"] ||
21+
env["secure_headers_content_security_policy_nonce"] ||
22+
""
23+
end
24+
1325
def generate_html(page_struct, env, result_json = page_struct.to_json)
1426
# double-assigning to suppress "assigned but unused variable" warnings
1527
path = path = "#{env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME']}#{@config.base_url_path}"
@@ -39,15 +51,6 @@ def get_profile_script(env, response_headers = {})
3951
url = "#{path}includes.js?v=#{version}" if !url
4052
css_url = "#{path}includes.css?v=#{version}" if !css_url
4153

42-
configured_nonce = @config.content_security_policy_nonce
43-
if configured_nonce && !configured_nonce.is_a?(String)
44-
configured_nonce = configured_nonce.call(env, response_headers)
45-
end
46-
47-
content_security_policy_nonce = configured_nonce ||
48-
env["action_dispatch.content_security_policy_nonce"] ||
49-
env["secure_headers_content_security_policy_nonce"]
50-
5154
settings = {
5255
path: path,
5356
url: url,
@@ -66,7 +69,7 @@ def get_profile_script(env, response_headers = {})
6669
collapseResults: @config.collapse_results,
6770
htmlContainer: @config.html_container,
6871
hiddenCustomFields: @config.snapshot_hidden_custom_fields.join(','),
69-
cspNonce: content_security_policy_nonce,
72+
cspNonce: get_csp_nonce(env, response_headers),
7073
hotwireTurboDriveSupport: @config.enable_hotwire_turbo_drive_support,
7174
}
7275

@@ -112,6 +115,8 @@ def make_link(postfix, env)
112115
def flamegraph(graph, path, env)
113116
headers = { 'content-type' => 'text/html' }
114117
iframe_src = "#{public_base_path(env)}speedscope/index.html"
118+
csp_nonce = get_csp_nonce(env, headers)
119+
115120
html = <<~HTML
116121
<!DOCTYPE html>
117122
<html>
@@ -123,7 +128,7 @@ def flamegraph(graph, path, env)
123128
</style>
124129
</head>
125130
<body>
126-
<script type="text/javascript">
131+
<script type="text/javascript" nonce="#{csp_nonce}">
127132
var graph = #{JSON.generate(graph)};
128133
var json = JSON.stringify(graph);
129134
var blob = new Blob([json], { type: 'text/plain' });

spec/integration/middleware_spec.rb

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require 'rack'
34
require 'rack/test'
45
require 'zlib'
56

@@ -280,5 +281,68 @@ def app
280281
expect(env["REQUEST_METHOD"]).to eq("GET")
281282
expect(response_headers["Content-Type"]).to eq("text/html")
282283
end
284+
285+
end
286+
287+
context 'flamegraph with CSP nonce' do
288+
class CSPMiddleware
289+
def initialize(app)
290+
@app = app
291+
end
292+
293+
def call(env)
294+
env["action_dispatch.content_security_policy_nonce"] = "railsflamenonce"
295+
@app.call(env)
296+
end
297+
end
298+
299+
def app
300+
Rack::Builder.new do
301+
use CSPMiddleware
302+
use Rack::MiniProfiler
303+
run lambda { |env|
304+
[200, { 'Content-Type' => 'text/html' }, ['<html><body><h1>Hello</h1></body></html>']]
305+
}
306+
end
307+
end
308+
309+
def do_flamegraph_test
310+
pid = fork do # Avoid polluting main process with stackprof
311+
require 'stackprof'
312+
313+
get '/html?pp=async-flamegraph'
314+
expect(last_response).to be_ok
315+
flamegraph_path = last_response.headers['X-MiniProfiler-Flamegraph-Path']
316+
317+
get flamegraph_path
318+
expect(last_response).to be_ok
319+
yield last_response.body
320+
end
321+
322+
Process.wait(pid)
323+
expect($?.exitstatus).to eq(0)
324+
end
325+
326+
it 'uses Rails value when available' do
327+
do_flamegraph_test do |body|
328+
expect(body).to include('<script type="text/javascript" nonce="railsflamenonce">')
329+
end
330+
end
331+
332+
it 'uses configured string when available' do
333+
Rack::MiniProfiler.config.content_security_policy_nonce = "configuredflamenonce"
334+
335+
do_flamegraph_test do |body|
336+
expect(body).to include('<script type="text/javascript" nonce="configuredflamenonce">')
337+
end
338+
end
339+
340+
it 'calls configured block when available' do
341+
Rack::MiniProfiler.config.content_security_policy_nonce = Proc.new { "dynamicflamenonce" }
342+
343+
do_flamegraph_test do |body|
344+
expect(body).to include('<script type="text/javascript" nonce="dynamicflamenonce">')
345+
end
346+
end
283347
end
284348
end

0 commit comments

Comments
 (0)