mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-08-28 09:32:37 +09:00
Harden custom-styles route: browser XSS proof and higher-resolution cache key
Add a headless-browser system test that stores a </style><script> payload in custom_styles, loads a page, and asserts the payload arrives as an external text/css stylesheet and never executes (no script breakout, no window flag set). It fails against the old inline rendering. Bust the stylesheet cache key with the repo's millisecond :epoch format so two saves within the same second still produce distinct URLs — the five-minute public cache would otherwise pin the first save.
This commit is contained in:
+1
-1
@@ -30,7 +30,7 @@ Rails.application.routes.draw do
|
||||
end
|
||||
|
||||
direct :fresh_custom_styles do |options|
|
||||
route_for :account_custom_styles, v: Current.account&.updated_at&.to_fs(:number)
|
||||
route_for :account_custom_styles, v: Current.account&.updated_at&.to_fs(:epoch)
|
||||
end
|
||||
|
||||
get "join/:join_code", to: "users#new", as: :join
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
require "application_system_test_case"
|
||||
|
||||
class CustomStylesXssTest < ApplicationSystemTestCase
|
||||
# A stored payload that breaks out of an inline <style> context and executes
|
||||
# as HTML. Rendered inline (the old behavior) this <script> runs and sets the
|
||||
# flag; delivered as an external text/css stylesheet it is inert text.
|
||||
PAYLOAD = %(</style><script>window.__xss_fired = true</script>)
|
||||
|
||||
setup do
|
||||
accounts(:signal).update!(custom_styles: PAYLOAD)
|
||||
sign_in "jz@37signals.com"
|
||||
end
|
||||
|
||||
test "custom styles payload loads as a stylesheet and never executes" do
|
||||
# (a) custom styles arrive via an external stylesheet link, not inline markup
|
||||
assert_selector "link[rel='stylesheet'][href*='custom_styles']", visible: false
|
||||
|
||||
# (b) the payload's <script> never executed — no breakout from CSS context
|
||||
assert_nil evaluate_script("window.__xss_fired")
|
||||
|
||||
# (b') and it injected no live <script> element into the document
|
||||
assert_no_selector "script", text: "__xss_fired", visible: false
|
||||
|
||||
# (c) the browser fetched the payload verbatim as text/css, so it is styled,
|
||||
# never parsed as HTML
|
||||
fetched = page.evaluate_async_script(<<~JS)
|
||||
var done = arguments[arguments.length - 1];
|
||||
var href = document.querySelector("link[rel='stylesheet'][href*='custom_styles']").href;
|
||||
fetch(href).then(function(r) {
|
||||
return r.text().then(function(body) {
|
||||
done({ type: r.headers.get("content-type"), body: body });
|
||||
});
|
||||
});
|
||||
JS
|
||||
|
||||
assert_includes fetched["type"], "text/css"
|
||||
assert_includes fetched["body"], PAYLOAD
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user