mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-09-01 11:21:31 +09:00
Serve custom styles from a dedicated text/css route
Admin-authored custom styles were rendered inline into every page via custom_styles_tag, marked html_safe inside a <style> element. A stored payload containing </style><script>…</script> broke out of the style context and executed as HTML — a stored XSS reaching everyone who loads the app. Deliver the styles as an external stylesheet instead, following the account logo pattern: a new Accounts::CustomStyles#show renders the raw CSS with a text/css content type (ETag on the account, 5-minute public cache with stale-while-revalidate, fresh_custom_styles cache buster on account updates), and the layout links it with <link rel="stylesheet" data-turbo-track="reload">. In a CSS document, markup is inert text — the HTML-context breakout is closed by construction, and admins keep full custom-CSS capability.
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
class Accounts::CustomStylesController < ApplicationController
|
||||
before_action :ensure_can_administer, :set_account
|
||||
allow_unauthenticated_access only: :show
|
||||
before_action :ensure_can_administer, :set_account, except: :show
|
||||
|
||||
def show
|
||||
if stale?(etag: Current.account)
|
||||
expires_in 5.minutes, public: true, stale_while_revalidate: 1.week
|
||||
|
||||
render plain: Current.account&.custom_styles.to_s, content_type: "text/css"
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
@@ -12,12 +12,6 @@ module ApplicationHelper
|
||||
end
|
||||
end
|
||||
|
||||
def custom_styles_tag
|
||||
if custom_styles = Current.account&.custom_styles
|
||||
tag.style(custom_styles.to_s.html_safe, data: { turbo_track: "reload" })
|
||||
end
|
||||
end
|
||||
|
||||
def body_classes
|
||||
[ @body_class, admin_body_class, account_logo_body_class ].compact.join(" ")
|
||||
end
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<%= tag.link rel: "apple-touch-icon", href: fresh_account_logo_path %>
|
||||
|
||||
<%= stylesheet_link_tag :all, "data-turbo-track": "reload" %>
|
||||
<%= custom_styles_tag %>
|
||||
<%= tag.link rel: "stylesheet", href: fresh_custom_styles_path, data: { turbo_track: "reload" } %>
|
||||
|
||||
<%= javascript_importmap_tags %>
|
||||
|
||||
|
||||
+5
-1
@@ -21,7 +21,7 @@ Rails.application.routes.draw do
|
||||
|
||||
resource :join_code, only: :create
|
||||
resource :logo, only: %i[ show destroy ]
|
||||
resource :custom_styles, only: %i[ edit update ]
|
||||
resource :custom_styles, only: %i[ show edit update ]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -29,6 +29,10 @@ Rails.application.routes.draw do
|
||||
route_for :account_logo, v: Current.account&.updated_at&.to_fs(:number), size: options[:size]
|
||||
end
|
||||
|
||||
direct :fresh_custom_styles do |options|
|
||||
route_for :account_custom_styles, v: Current.account&.updated_at&.to_fs(:number)
|
||||
end
|
||||
|
||||
get "join/:join_code", to: "users#new", as: :join
|
||||
post "join/:join_code", to: "users#create"
|
||||
|
||||
|
||||
@@ -5,6 +5,34 @@ class Accounts::CustomStylesControllerTest < ActionDispatch::IntegrationTest
|
||||
sign_in :david
|
||||
end
|
||||
|
||||
test "show serves custom styles as plain CSS" do
|
||||
accounts(:signal).update! custom_styles: ":root { --color-text: red; }"
|
||||
|
||||
get account_custom_styles_url
|
||||
assert_response :ok
|
||||
assert_equal "text/css", @response.media_type
|
||||
assert_equal ":root { --color-text: red; }", @response.body
|
||||
end
|
||||
|
||||
test "show is accessible without authentication" do
|
||||
accounts(:signal).update! custom_styles: ":root { --color-text: red; }"
|
||||
reset!
|
||||
|
||||
get account_custom_styles_url
|
||||
assert_response :ok
|
||||
assert_equal "text/css", @response.media_type
|
||||
end
|
||||
|
||||
test "show serves markup verbatim as inert CSS text, never HTML" do
|
||||
payload = "</style><script>alert(1)</script>"
|
||||
accounts(:signal).update! custom_styles: payload
|
||||
|
||||
get account_custom_styles_url
|
||||
assert_response :ok
|
||||
assert_equal "text/css", @response.media_type
|
||||
assert_equal payload, @response.body
|
||||
end
|
||||
|
||||
test "edit" do
|
||||
get edit_account_custom_styles_url
|
||||
assert_response :ok
|
||||
|
||||
Reference in New Issue
Block a user