diff --git a/app/controllers/concerns/inline_script_hashes.rb b/app/controllers/concerns/inline_script_hashes.rb index 357a70d..4577292 100644 --- a/app/controllers/concerns/inline_script_hashes.rb +++ b/app/controllers/concerns/inline_script_hashes.rb @@ -24,7 +24,9 @@ module InlineScriptHashes # 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 - # is allowed up to its asset directory; one outside it is allowed exactly. + # is allowed up to its asset directory, the last one in its path, since an + # asset host's own path may contain one too; a URL outside it is allowed + # exactly. # # This origin's own sources leave out the scheme, so they match the page's # scheme even behind a proxy that terminates TLS without saying so. @@ -37,7 +39,7 @@ module InlineScriptHashes JSON.parse(importmap)["imports"].values.map do |url| url = URI.join(request.base_url, url) origin = url.origin == request.base_url ? request.host_with_port : url.origin - directory = url.path[0, url.path.index(assets) + assets.length] if url.path.include?(assets) + directory = url.path[0, url.path.rindex(assets) + assets.length] if url.path.include?(assets) "#{origin}#{directory || url.path}" end.uniq end diff --git a/test/integration/content_security_policy_test.rb b/test/integration/content_security_policy_test.rb index b4672e0..7a3aaf6 100644 --- a/test/integration/content_security_policy_test.rb +++ b/test/integration/content_security_policy_test.rb @@ -56,6 +56,27 @@ class ContentSecurityPolicyTest < ActionDispatch::IntegrationTest end end + test "an asset host whose path contains the asset directory is limited to the directory it serves" do + with_asset_host("https://cdn.example.test/assets") do + get PAGE_PATH + follow_redirect! while response.redirect? + + script_src = response.headers["Content-Security-Policy"][/script-src ([^;]+)/, 1].split + assert_includes script_src, "https://cdn.example.test/assets/assets/" + assert_not allowed_by_path?(script_src, "https://cdn.example.test/assets/rails/active_storage/blobs/proxy/x/upload.js") + end + end + + test "the sign-in page is limited to the asset directory too" do + cookies.delete(:session_token) + get new_session_url + assert_response :success + + script_src = response.headers["Content-Security-Policy"][/script-src ([^;]+)/, 1].split + assert_not_includes script_src, "'self'" + assert_includes script_src, "www.example.com/assets/" + end + test "scripts are limited to the asset directory under a relative root" do with_relative_url_root("/chat") do get PAGE_PATH