mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-10-28 00:16:48 +09:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84ad886114 | ||
|
|
2fd1483e16 | ||
|
|
424219e485 |
No files matched your search
@@ -1,5 +1,14 @@
|
|||||||
class Accounts::CustomStylesController < ApplicationController
|
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
|
def edit
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,12 +12,6 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
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
|
def body_classes
|
||||||
[ @body_class, admin_body_class, account_logo_body_class ].compact.join(" ")
|
[ @body_class, admin_body_class, account_logo_body_class ].compact.join(" ")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<%= tag.link rel: "apple-touch-icon", href: fresh_account_logo_path %>
|
<%= tag.link rel: "apple-touch-icon", href: fresh_account_logo_path %>
|
||||||
|
|
||||||
<%= stylesheet_link_tag :all, "data-turbo-track": "reload" %>
|
<%= 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 %>
|
<%= javascript_importmap_tags %>
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -21,7 +21,7 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
resource :join_code, only: :create
|
resource :join_code, only: :create
|
||||||
resource :logo, only: %i[ show destroy ]
|
resource :logo, only: %i[ show destroy ]
|
||||||
resource :custom_styles, only: %i[ edit update ]
|
resource :custom_styles, only: %i[ show edit update ]
|
||||||
end
|
end
|
||||||
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]
|
route_for :account_logo, v: Current.account&.updated_at&.to_fs(:number), size: options[:size]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
direct :fresh_custom_styles do |options|
|
||||||
|
route_for :account_custom_styles, v: Current.account&.updated_at&.to_fs(:epoch)
|
||||||
|
end
|
||||||
|
|
||||||
get "join/:join_code", to: "users#new", as: :join
|
get "join/:join_code", to: "users#new", as: :join
|
||||||
post "join/:join_code", to: "users#create"
|
post "join/:join_code", to: "users#create"
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,34 @@ class Accounts::CustomStylesControllerTest < ActionDispatch::IntegrationTest
|
|||||||
sign_in :david
|
sign_in :david
|
||||||
end
|
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
|
test "edit" do
|
||||||
get edit_account_custom_styles_url
|
get edit_account_custom_styles_url
|
||||||
assert_response :ok
|
assert_response :ok
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
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
|
||||||
|
# Navigate to the page under test explicitly (matching the writebook
|
||||||
|
# sibling) rather than leaning on the post-login landing page — otherwise
|
||||||
|
# this coverage could silently evaporate if that landing changes.
|
||||||
|
visit root_url
|
||||||
|
|
||||||
|
# (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