unsafe-inline on script-src neutered CSP as an XSS defense on the login and OAuth consent pages (the highest-value targets in an IdP). Switch to a per-response nonce for both script-src and style-src and drop unsafe-inline entirely. - Add a random per-response nonce generator and apply it to script-src/style-src. - Remove :unsafe_inline from both directives. - Nonce the one hand-written inline script (dark-mode detection in the layout). - Convert the 2 static style="display:none" attributes to class="hidden" (their runtime toggle is done via element.style in JS, which CSP does not govern). importmap-rails (2.2.3) already stamps the nonce onto its generated inline importmap/module/preload tags, and Turbo (2.0.23) reads csp_meta_tag for its injected <style>, so no other view changes were needed. Adds an integration test asserting the enforcing header carries nonces, omits unsafe-inline, and that the inline script's nonce matches the header. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
41 lines
1.5 KiB
Ruby
41 lines
1.5 KiB
Ruby
require "test_helper"
|
|
|
|
class CspTest < ActionDispatch::IntegrationTest
|
|
# In the test env content_security_policy_report_only is false, so the enforcing
|
|
# Content-Security-Policy header is emitted.
|
|
test "signin page sends a nonce-based CSP with no unsafe-inline" do
|
|
get signin_path
|
|
assert_response :success
|
|
|
|
csp = response.headers["Content-Security-Policy"]
|
|
assert csp.present?, "expected a Content-Security-Policy header"
|
|
|
|
script_src = directive(csp, "script-src")
|
|
style_src = directive(csp, "style-src")
|
|
|
|
assert_includes script_src, "'nonce-", "script-src must carry a nonce"
|
|
assert_includes style_src, "'nonce-", "style-src must carry a nonce"
|
|
refute_includes script_src, "'unsafe-inline'", "script-src must not allow unsafe-inline"
|
|
refute_includes style_src, "'unsafe-inline'", "style-src must not allow unsafe-inline"
|
|
end
|
|
|
|
test "the inline theme script carries the script-src nonce" do
|
|
get signin_path
|
|
assert_response :success
|
|
|
|
header_nonce = response.headers["Content-Security-Policy"][/script-src[^;]*'nonce-([^']+)'/, 1]
|
|
assert header_nonce.present?, "expected a nonce in the CSP header"
|
|
|
|
# The hand-written dark-mode <script> in the layout must use the same nonce,
|
|
# otherwise it would be blocked under the enforcing policy.
|
|
assert_match(/<script nonce="#{Regexp.escape(header_nonce)}">/, response.body,
|
|
"inline theme script must carry the matching CSP nonce")
|
|
end
|
|
|
|
private
|
|
|
|
def directive(csp, name)
|
|
csp.split(";").map(&:strip).find { |d| d.start_with?("#{name} ") } || ""
|
|
end
|
|
end
|