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`.
This commit is contained in:
Emi Matchu 2025-03-29 14:45:41 -07:00
parent c48b2b14aa
commit 38bad12778

View file

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