Compute a page's script sources once per importmap and host

Every page parsed the importmap and joined each of its URLs against the
request's base URL, about a millisecond of CPU a page, though the answer
changes only with the importmap and the host. Keep the last answer, keyed
by both. One entry, so a request with a made-up Host header can't grow it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sam RubyandClaude Opus 5.5 authored and Rosa Gutierrez committed 2026-10-21 21:06:01 +09:00
1 parent 8d4a5264cf
commit c8f74a0b0e
2 files changed
+30 -1

No files matched your search

@@ -14,13 +14,32 @@ module InlineScriptHashes
content_security_policy if: -> { request.content_security_policy } do |policy|
if sources = policy.directives["script-src"]
importmap = Rails.application.importmap.to_json(resolver: helpers)
policy.directives["script-src"] = (sources - [ "'self'" ]) | script_locations(importmap) | script_hashes(importmap, ENTRY_POINT_SCRIPT)
policy.directives["script-src"] = (sources - [ "'self'" ]) | script_sources(importmap)
policy.worker_src "#{request.host_with_port}#{service_worker_path(format: :js)}"
end
end
end
mattr_accessor :last_script_sources
private
# Parsing the importmap and resolving each of its URLs costs about a
# millisecond a page, yet the answer changes only with the importmap and
# the host the page is served from. Keep the last one: a single entry, so
# a request with a made-up Host header can't grow it.
def script_sources(importmap)
key = [ importmap, request.base_url, request.host_with_port, config.relative_url_root ]
cached_key, sources = InlineScriptHashes.last_script_sources
if cached_key == key
sources
else
(script_locations(importmap) | script_hashes(importmap, ENTRY_POINT_SCRIPT)).freeze.tap do |sources|
InlineScriptHashes.last_script_sources = [ key, sources ].freeze
end
end
end
# The importmap is resolved by Rails' asset helpers, so its URLs already
# carry any relative root and asset host, however the host is written:
# with or without a scheme, protocol-relative, a %d set, or a proc. Each
@@ -103,6 +103,16 @@ class ContentSecurityPolicyTest < ActionDispatch::IntegrationTest
assert_equal "www.example.com:8443/service-worker.js", policy[/worker-src ([^;]+)/, 1]
end
test "each host's pages carry that host's sources, one after another" do
[ "www.example.com", "www.example.com:8443", "www.example.com" ].each do |host|
host! host
get PAGE_PATH
follow_redirect! while response.redirect?
assert_includes response.headers["Content-Security-Policy"][/script-src ([^;]+)/, 1].split, "#{host}/assets/"
end
end
test "uploads can't load as scripts" do
get PAGE_PATH
follow_redirect! while response.redirect?