Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG

## Unreleased

- [FIX] Add CSP nonce support to flamegraph rendering [#648](https://github.com/MiniProfiler/rack-mini-profiler/pull/648)

## 4.0.1 - 2025-07-31

- [FIX] Ensure Rack 2 / 3 cross compatibility [#653](https://github.com/MiniProfiler/rack-mini-profiler/pull/653)
Expand Down
27 changes: 16 additions & 11 deletions lib/mini_profiler/views.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ def share_template
@share_template ||= ERB.new(::File.read(::File.expand_path("../html/share.html", ::File.dirname(__FILE__))))
end

def get_csp_nonce(env, response_headers = {})
configured_nonce = @config.content_security_policy_nonce
if configured_nonce && !configured_nonce.is_a?(String)
configured_nonce = configured_nonce.call(env, response_headers)
end

configured_nonce ||
env["action_dispatch.content_security_policy_nonce"] ||
env["secure_headers_content_security_policy_nonce"] ||
""
end

def generate_html(page_struct, env, result_json = page_struct.to_json)
# double-assigning to suppress "assigned but unused variable" warnings
path = path = "#{env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME']}#{@config.base_url_path}"
Expand Down Expand Up @@ -39,15 +51,6 @@ def get_profile_script(env, response_headers = {})
url = "#{path}includes.js?v=#{version}" if !url
css_url = "#{path}includes.css?v=#{version}" if !css_url

configured_nonce = @config.content_security_policy_nonce
if configured_nonce && !configured_nonce.is_a?(String)
configured_nonce = configured_nonce.call(env, response_headers)
end

content_security_policy_nonce = configured_nonce ||
env["action_dispatch.content_security_policy_nonce"] ||
env["secure_headers_content_security_policy_nonce"]

settings = {
path: path,
url: url,
Expand All @@ -66,7 +69,7 @@ def get_profile_script(env, response_headers = {})
collapseResults: @config.collapse_results,
htmlContainer: @config.html_container,
hiddenCustomFields: @config.snapshot_hidden_custom_fields.join(','),
cspNonce: content_security_policy_nonce,
cspNonce: get_csp_nonce(env, response_headers),
hotwireTurboDriveSupport: @config.enable_hotwire_turbo_drive_support,
}

Expand Down Expand Up @@ -112,6 +115,8 @@ def make_link(postfix, env)
def flamegraph(graph, path, env)
headers = { 'content-type' => 'text/html' }
iframe_src = "#{public_base_path(env)}speedscope/index.html"
csp_nonce = get_csp_nonce(env, headers)

html = <<~HTML
<!DOCTYPE html>
<html>
Expand All @@ -123,7 +128,7 @@ def flamegraph(graph, path, env)
</style>
</head>
<body>
<script type="text/javascript">
<script type="text/javascript" nonce="#{csp_nonce}">
var graph = #{JSON.generate(graph)};
var json = JSON.stringify(graph);
var blob = new Blob([json], { type: 'text/plain' });
Expand Down
64 changes: 64 additions & 0 deletions spec/integration/middleware_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true

require 'rack'
require 'rack/test'
require 'zlib'

Expand Down Expand Up @@ -280,5 +281,68 @@ def app
expect(env["REQUEST_METHOD"]).to eq("GET")
expect(response_headers[Rack::CONTENT_TYPE]).to eq("text/html")
end

end

context 'flamegraph with CSP nonce' do
class CSPMiddleware
def initialize(app)
@app = app
end

def call(env)
env["action_dispatch.content_security_policy_nonce"] = "railsflamenonce"
@app.call(env)
end
end

def app
Rack::Builder.new do
use CSPMiddleware
use Rack::MiniProfiler
run lambda { |env|
[200, { 'Content-Type' => 'text/html' }, ['<html><body><h1>Hello</h1></body></html>']]
}
end
end

def do_flamegraph_test
pid = fork do # Avoid polluting main process with stackprof
require 'stackprof'

get '/html?pp=async-flamegraph'
expect(last_response).to be_ok
flamegraph_path = last_response.headers['X-MiniProfiler-Flamegraph-Path']

get flamegraph_path
expect(last_response).to be_ok
yield last_response.body
end

Process.wait(pid)
expect($?.exitstatus).to eq(0)
end

it 'uses Rails value when available' do
do_flamegraph_test do |body|
expect(body).to include('<script type="text/javascript" nonce="railsflamenonce">')
end
end

it 'uses configured string when available' do
Rack::MiniProfiler.config.content_security_policy_nonce = "configuredflamenonce"

do_flamegraph_test do |body|
expect(body).to include('<script type="text/javascript" nonce="configuredflamenonce">')
end
end

it 'calls configured block when available' do
Rack::MiniProfiler.config.content_security_policy_nonce = Proc.new { "dynamicflamenonce" }

do_flamegraph_test do |body|
expect(body).to include('<script type="text/javascript" nonce="dynamicflamenonce">')
end
end
end
end