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`.
67 lines
2.1 KiB
Ruby
67 lines
2.1 KiB
Ruby
class SwfAssetsController < ApplicationController
|
|
# We're very careful with what content is allowed to load. This is because
|
|
# asset movies run arbitrary JS, and, while we generally trust content from
|
|
# Neopets.com, let's not be *allowing* movie JS to do whatever it wants! This
|
|
# is a good default security stance, even if we don't foresee an attack.
|
|
content_security_policy do |policy|
|
|
policy.sandbox "allow-scripts"
|
|
policy.default_src "none"
|
|
|
|
policy.img_src -> {
|
|
src_list(
|
|
helpers.image_url("favicon.png"),
|
|
@swf_asset.image_url,
|
|
*@swf_asset.canvas_movie_sprite_urls,
|
|
|
|
# For images, `images.neopets.com` is a generally safe host to load
|
|
# from (shouldn't be a vulnerable site or exfiltration vector), and
|
|
# 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".)
|
|
origins: ["https://images.neopets.com"],
|
|
)
|
|
}
|
|
|
|
policy.script_src -> {
|
|
src_list(
|
|
helpers.javascript_url("easeljs.min"),
|
|
helpers.javascript_url("tweenjs.min"),
|
|
helpers.javascript_url("swf_assets/show"),
|
|
@swf_asset.canvas_movie_library_url,
|
|
)
|
|
}
|
|
|
|
policy.style_src -> {
|
|
src_list(
|
|
helpers.stylesheet_url("swf_assets/show"),
|
|
)
|
|
}
|
|
end
|
|
|
|
def show
|
|
@swf_asset = SwfAsset.find params[:id]
|
|
render layout: nil
|
|
end
|
|
|
|
private
|
|
|
|
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)
|
|
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
|