Allow an asset host only up to its own asset directory

An asset host's path can contain the asset directory too, as in
https://cdn.example.com/assets, and cutting each URL at the first one would
allow everything the host serves under it. Cut at the last one instead.

(cherry picked from commit 2ba940c683)
This commit is contained in:
Jeremy Daer committed 2026-10-17 00:47:41 -07:00
1 parent 26b3d2abb0
commit 7a5931fe2a
2 files changed
+25 -2

No files matched your search

@@ -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
@@ -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