From 38bad12778e4498762e81ac64f6a1a862804d317 Mon Sep 17 00:00:00 2001 From: Emi Matchu Date: Sat, 29 Mar 2025 14:45:41 -0700 Subject: [PATCH] Fix item animations for asset URLs that contain unescaped spaces Before this change, the "Ornamental Lake with Goldies" item would fail to preview on the item page: the iframe for the animation layer would display an error page. The error was: ``` Invalid Content Security Policy script-src: "https://images.neopets.com/cp/items/data/000/000/497/497366_deca9f2827/497366_HTML5 Canvas.js". Directive values must not contain whitespace or semicolons. Please use multiple arguments or other directive methods instead. (ActionDispatch::ContentSecurityPolicy::InvalidDirectiveError) ``` This is because the URL that Neopets sends us for this JS file contains an unescaped space character. This isn't usually an issue for e.g. loading a URL in the browser, but it's *not* valid syntax for inclusion in a Content Security Policy. In this change, we update our CSP code to parse URLs into `Addressable::URI` objects, which enables us to call the `normalize!` method, which fixes oddities like that. The URL now correctly appears in the CSP as `https://images.neopets.com/cp/items/data/000/000/497/497366_deca9f2827/497366_HTML5%20Canvas.js`. --- app/controllers/swf_assets_controller.rb | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/app/controllers/swf_assets_controller.rb b/app/controllers/swf_assets_controller.rb index 4803d3ef..3b3ce148 100644 --- a/app/controllers/swf_assets_controller.rb +++ b/app/controllers/swf_assets_controller.rb @@ -18,7 +18,7 @@ class SwfAssetsController < ApplicationController # doing this can help make this header a *lot* shorter, which helps # our nginx reverse proxy (and probably some clients) handle it. (For # example, see asset `667993` for "Engulfed in Flames Effect".) - hosts: ["https://images.neopets.com"], + origins: ["https://images.neopets.com"], ) } @@ -45,14 +45,23 @@ class SwfAssetsController < ApplicationController private - def src_list(*urls, hosts: []) - urls. + def src_list(*urls, origins: []) + clean_urls = urls. # Ignore any `nil`s that might arise filter(&:present?). + # Parse the URL. + map { |url| Addressable::URI.parse(url) }. # Remove query strings from URLs (they're invalid in CSPs) - map { |url| url.sub(/\?.*\z/, "") }. - # For the given `hosts`, remove all their specific URLs, and just list - # the host itself. - reject { |url| hosts.any? { |h| url.start_with? h } } + hosts + each { |url| url.query = nil }. + # For the given `origins`, remove all their specific URLs, because + # we'll just include the entire origin anyway. + reject { |url| origins.include?(url.origin) }. + # Normalize the URLs. (This fixes issues like when the canonical + # Neopets version of the URL contains plain unescaped spaces.) + each(&:normalize!). + # Convert the URLs back into strings. + map(&:to_s) + + clean_urls + origins end end