mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-10-27 16:06:48 +09:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1376a97ee | ||
|
|
d3f22d78e1 |
No files matched your search
+4
-4
@@ -375,10 +375,10 @@ GEM
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.1.8)
|
||||
thor (1.4.0)
|
||||
thruster (0.1.15-aarch64-linux)
|
||||
thruster (0.1.15-arm64-darwin)
|
||||
thruster (0.1.15-x86_64-darwin)
|
||||
thruster (0.1.15-x86_64-linux)
|
||||
thruster (0.1.23-aarch64-linux)
|
||||
thruster (0.1.23-arm64-darwin)
|
||||
thruster (0.1.23-x86_64-darwin)
|
||||
thruster (0.1.23-x86_64-linux)
|
||||
tilt (2.6.1)
|
||||
timeout (0.6.1)
|
||||
tsort (0.2.0)
|
||||
|
||||
@@ -2,7 +2,6 @@ class Accounts::LogosController < ApplicationController
|
||||
include ActiveStorage::Streaming, ActionView::Helpers::AssetUrlHelper
|
||||
|
||||
allow_unauthenticated_access only: :show
|
||||
content_security_policy false, only: :show
|
||||
before_action :ensure_can_administer, only: :destroy
|
||||
|
||||
def show
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
class QrCodeController < ApplicationController
|
||||
allow_unauthenticated_access
|
||||
content_security_policy false
|
||||
|
||||
def show
|
||||
url = Base64.urlsafe_decode64(params[:id])
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
class Users::AvatarsController < ApplicationController
|
||||
include ActiveStorage::Streaming
|
||||
|
||||
content_security_policy false, only: :show
|
||||
|
||||
rescue_from(ActiveSupport::MessageVerifier::InvalidSignature) { head :not_found }
|
||||
|
||||
def show
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
module ContentFilters
|
||||
TextMessagePresentationFilters = ActionText::Content::Filters.new(RemoveSoloUnfurledLinkText, StyleUnfurledTwitterAvatars, SanitizeTags)
|
||||
TextMessagePresentationFilters = ActionText::Content::Filters.new(RemoveSoloUnfurledLinkText, StyleUnfurledTwitterAvatars, SanitizeTags, SanitizeAttributes)
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
class ContentFilters::SanitizeAttributes < ActionText::Content::Filter
|
||||
def applicable?
|
||||
true
|
||||
end
|
||||
|
||||
# Scrub attributes on the tags that SanitizeTags allows, using Rails' safe-list
|
||||
# sanitizer so unsafe URI schemes and event-handler attributes are stripped.
|
||||
# Runs after SanitizeTags, so passing the same allowed tags makes the tag pass
|
||||
# a no-op and only attributes are scrubbed.
|
||||
def apply
|
||||
sanitizer.sanitize fragment.to_html, tags: allowed_tags, attributes: allowed_attributes
|
||||
end
|
||||
|
||||
private
|
||||
# Presentation styling relies on class attributes (e.g. unfurled link embeds),
|
||||
# which the standard ActionText set doesn't include.
|
||||
EXTRA_ALLOWED_ATTRIBUTES = %w[ class ]
|
||||
|
||||
# ActionText::ContentHelper.sanitizer is a single process-wide instance, and
|
||||
# rails-html-sanitizer reuses one mutable permit scrubber that it reconfigures
|
||||
# from the tags/attributes on every #sanitize call. Sharing it here would race
|
||||
# our stricter tag set against ActionText's default rendering on concurrent
|
||||
# requests, so use a dedicated instance of the same sanitizer class to keep the
|
||||
# scrubber isolated.
|
||||
def sanitizer
|
||||
sanitizer_class.new
|
||||
end
|
||||
|
||||
def sanitizer_class
|
||||
ActionText::ContentHelper.sanitizer.class
|
||||
end
|
||||
|
||||
def allowed_tags
|
||||
ContentFilters::SanitizeTags::ALLOWED_TAGS
|
||||
end
|
||||
|
||||
def allowed_attributes
|
||||
standard_allowed_attributes + EXTRA_ALLOWED_ATTRIBUTES
|
||||
end
|
||||
|
||||
# Mirrors ActionText::ContentHelper#sanitizer_allowed_attributes, which isn't
|
||||
# exposed at the module level.
|
||||
def standard_allowed_attributes
|
||||
ActionText::ContentHelper.allowed_attributes ||
|
||||
(sanitizer_class.allowed_attributes + ActionText::Attachment::ATTRIBUTES).to_a
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,7 @@
|
||||
<%# Be sure to check/update messages/_template.html.erb when changing this file %>
|
||||
|
||||
<% cache message do %>
|
||||
<%# Bump this version when the message presentation filters change what they emit. Editing this line changes the template digest, which busts BOTH this fragment cache and the collection cache that keys on this partial's digest (helper Ruby changes alone don't). %>
|
||||
<% cache [ message, "presentation-v2" ] do %>
|
||||
<%= message_tag message do %>
|
||||
<h2 class="message__day-separator"><%= local_datetime_tag message.created_at, style: :date %></h2>
|
||||
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
||||
pin "@hotwired/turbo-rails", to: "turbo.js"
|
||||
pin "@rails/actioncable", to: "actioncable.esm.js"
|
||||
pin "@rails/request.js", to: "@rails--request.js" # @0.0.8
|
||||
pin "trix", to: "trix.esm.min.js" # @2.1.19
|
||||
pin "trix", to: "trix.js"
|
||||
pin "@rails/actiontext", to: "actiontext.js"
|
||||
pin "highlight.js", to: "highlight.js/core.js"
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
ActiveSupport.on_load(:active_storage_blob) do
|
||||
ActiveStorage::DiskController.content_security_policy false, only: :show
|
||||
|
||||
ActiveStorage::DiskController.after_action only: :show do
|
||||
response.set_header("Cache-Control", "max-age=3600, public")
|
||||
end
|
||||
|
||||
@@ -1,120 +1,25 @@
|
||||
# Be sure to restart your server when you modify this file.
|
||||
|
||||
# Baseline application-wide Content-Security-Policy, deployed in Report-Only
|
||||
# mode: browsers evaluate the policy and report violations (once a report
|
||||
# endpoint is wired up) but enforce nothing, so rendering cannot break.
|
||||
# Tune the policy against observed violations, then flip
|
||||
# `content_security_policy_report_only` off to enforce it.
|
||||
#
|
||||
# Define an application-wide content security policy.
|
||||
# See the Securing Rails Applications Guide for more information:
|
||||
# https://guides.rubyonrails.org/security.html#content-security-policy-header
|
||||
|
||||
# Session-stable nonce for permitted inline scripts (the importmap JSON + shim,
|
||||
# auto-nonced by importmap-rails).
|
||||
# Rails.application.configure do
|
||||
# config.content_security_policy do |policy|
|
||||
# policy.default_src :self, :https
|
||||
# policy.font_src :self, :https, :data
|
||||
# policy.img_src :self, :https, :data
|
||||
# policy.object_src :none
|
||||
# policy.script_src :self, :https
|
||||
# policy.style_src :self, :https
|
||||
# # Specify URI for violation reports
|
||||
# # policy.report_uri "/csp-violation-report-endpoint"
|
||||
# end
|
||||
#
|
||||
# The nonce is stable across a session's requests so Turbo snapshot restores
|
||||
# don't replay a stale nonce and trip CSP. It's the HMAC of a stable cookie
|
||||
# value keyed by the server secret: the cookie is client-settable, but the
|
||||
# client can't predict the resulting nonce without knowing secret_key_base.
|
||||
# # Generate session nonces for permitted importmap and inline scripts
|
||||
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
|
||||
# config.content_security_policy_nonce_directives = %w(script-src)
|
||||
#
|
||||
# Campfire sets no per-session verification cookie, so the lightweight
|
||||
# nonce_id cookie — set on first visit, present for every session including
|
||||
# unauthenticated ones — is the sole identifier.
|
||||
#
|
||||
# The generator runs for every response that carries the policy, so the cookie
|
||||
# write would otherwise land on public, shared-cacheable responses (QR codes,
|
||||
# logos, avatars, disk-served blobs) where a cache could replay one client's
|
||||
# identifier and nonce to others. Those endpoints opt out of the policy with
|
||||
# `content_security_policy false`; the nonce is only meaningful on documents.
|
||||
module CSP
|
||||
module Nonce
|
||||
COOKIE = "campfire_csp_nonce_id"
|
||||
|
||||
def self.generate(request)
|
||||
hmac(nonce_id(request))
|
||||
end
|
||||
|
||||
def self.hmac(identifier)
|
||||
OpenSSL::HMAC.hexdigest("SHA256", Rails.application.secret_key_base, identifier)
|
||||
end
|
||||
|
||||
# Read or initialize a stable nonce identifier cookie.
|
||||
def self.nonce_id(request)
|
||||
request.cookies[COOKIE] || set_nonce_id(request)
|
||||
end
|
||||
|
||||
def self.set_nonce_id(request)
|
||||
value = SecureRandom.base64(16)
|
||||
request.cookie_jar[COOKIE] = { value: value, httponly: true, same_site: :lax }
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
# Per-install CSP extras.
|
||||
#
|
||||
# Campfire is a ONCE product: each customer self-hosts it on their own domain
|
||||
# and an admin may embed or connect to external hosts — video/embed providers,
|
||||
# image CDNs, analytics, form or webhook endpoints — that vary per install and
|
||||
# are unknown at build time. `:self` already tracks this install's own origin;
|
||||
# these ENV knobs let an admin allow additional hosts without editing this file
|
||||
# (and without which enforcement would break their legitimate integrations).
|
||||
#
|
||||
# Each is a comma-, semicolon-, or whitespace-separated list of CSP source
|
||||
# expressions, e.g.
|
||||
#
|
||||
# CSP_EXTRA_FRAME_SRC="https://www.youtube.com https://player.vimeo.com"
|
||||
#
|
||||
# Leave them unset (the default) to keep each directive at :self only.
|
||||
EXTRA_ENV = {
|
||||
script_src: "CSP_EXTRA_SCRIPT_SRC",
|
||||
style_src: "CSP_EXTRA_STYLE_SRC",
|
||||
img_src: "CSP_EXTRA_IMG_SRC",
|
||||
connect_src: "CSP_EXTRA_CONNECT_SRC",
|
||||
frame_src: "CSP_EXTRA_FRAME_SRC",
|
||||
form_action: "CSP_EXTRA_FORM_ACTION"
|
||||
}.freeze
|
||||
|
||||
# Parse one ENV knob into a list of extra host sources.
|
||||
#
|
||||
# Semicolons are tokenized like commas/whitespace: because the nonce forces a
|
||||
# per-request policy build, a stray `;` in a value (a plausible operator paste,
|
||||
# e.g. "https://youtube.com; https://vimeo.com") would otherwise land inside a
|
||||
# single source token and make Rails raise InvalidDirectiveError on every
|
||||
# request — a site-wide 500 even in report-only mode. Splitting on `;` yields
|
||||
# valid tokens instead; Rails still validates each one, so no injection is
|
||||
# introduced (a token with an embedded space still fails validation).
|
||||
def self.extra(directive)
|
||||
ENV[EXTRA_ENV.fetch(directive)].to_s.split(/[,;\s]+/).reject(&:blank?)
|
||||
end
|
||||
|
||||
# Build the baseline policy. Kept as a reusable method so it can be exercised
|
||||
# in isolation by tests as well as at boot.
|
||||
def self.apply(policy)
|
||||
policy.default_src :self
|
||||
policy.script_src :self, *extra(:script_src) # nonce auto-appended via nonce_directives below
|
||||
# unsafe_inline retained: many style="…" attributes across the message
|
||||
# composer and ActionText content can't be nonced yet.
|
||||
policy.style_src :self, :unsafe_inline, *extra(:style_src)
|
||||
# frame_src / img_src / connect_src start at :self plus any per-install extras
|
||||
# and get tuned against violation reports during the report-only window.
|
||||
policy.img_src :self, :data, :blob, *extra(:img_src)
|
||||
policy.connect_src :self, *extra(:connect_src)
|
||||
policy.frame_src :self, *extra(:frame_src)
|
||||
policy.frame_ancestors :self
|
||||
policy.base_uri :self
|
||||
policy.form_action :self, *extra(:form_action)
|
||||
policy.object_src :none
|
||||
# Specify URI for violation reports once a report sink is available
|
||||
# policy.report_uri "/csp-violation-report-endpoint"
|
||||
end
|
||||
end
|
||||
|
||||
Rails.application.configure do
|
||||
config.content_security_policy { |policy| CSP.apply(policy) }
|
||||
|
||||
config.content_security_policy_nonce_generator = ->(request) { CSP::Nonce.generate(request) }
|
||||
config.content_security_policy_nonce_directives = %w[ script-src ]
|
||||
|
||||
# Report violations without enforcing the policy.
|
||||
config.content_security_policy_report_only = true
|
||||
end
|
||||
# # Report violations without enforcing the policy.
|
||||
# # config.content_security_policy_report_only = true
|
||||
# end
|
||||
@@ -61,6 +61,75 @@ class ContentFiltersTest < ActionView::TestCase
|
||||
assert_equal "Hello World", filtered.to_html
|
||||
end
|
||||
|
||||
test "message with a link using an unsafe URI scheme" do
|
||||
message = Message.create! room: rooms(:pets), body: '<div><a href="javascript:alert(1)">x</a></div>', client_message_id: "0015", creator: users(:jason)
|
||||
|
||||
filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
|
||||
assert_no_match /javascript:/, filtered.to_html
|
||||
assert_match /<a>x<\/a>/, filtered.to_html
|
||||
end
|
||||
|
||||
test "message with an event handler attribute on an allowed tag" do
|
||||
message = Message.create! room: rooms(:pets), body: '<div><a href="/x" onmouseover="alert(1)">x</a> <span onclick="alert(2)">y</span></div>', client_message_id: "0015", creator: users(:jason)
|
||||
|
||||
filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
|
||||
assert_no_match /onmouseover/, filtered.to_html
|
||||
assert_no_match /onclick/, filtered.to_html
|
||||
assert_match /<a href="\/x">x<\/a>/, filtered.to_html
|
||||
assert_match /<span>y<\/span>/, filtered.to_html
|
||||
end
|
||||
|
||||
test "message with a data URI link" do
|
||||
message = Message.create! room: rooms(:pets), body: '<div><a href="data:text/html,pwned">x</a></div>', client_message_id: "0015", creator: users(:jason)
|
||||
|
||||
filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
|
||||
assert_no_match /data:/, filtered.to_html
|
||||
assert_match /<a>x<\/a>/, filtered.to_html
|
||||
end
|
||||
|
||||
test "message with a safe link and formatting is preserved" do
|
||||
body = '<div><a href="https://example.com">example</a> <strong>bold</strong> <code>code</code><ul><li>one</li><li>two</li></ul></div>'
|
||||
message = Message.create! room: rooms(:pets), body: body, client_message_id: "0015", creator: users(:jason)
|
||||
|
||||
filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
|
||||
assert_match /<a href="https:\/\/example\.com">example<\/a>/, filtered.to_html
|
||||
assert_match /<strong>bold<\/strong>/, filtered.to_html
|
||||
assert_match /<code>code<\/code>/, filtered.to_html
|
||||
assert_match /<ul>\s*<li>one<\/li>\s*<li>two<\/li>\s*<\/ul>/, filtered.to_html
|
||||
end
|
||||
|
||||
test "SanitizeAttributes uses an isolated sanitizer rather than the shared ActionText instance" do
|
||||
filter = ContentFilters::SanitizeAttributes.new(ActionText::Content.new("<div>x</div>"))
|
||||
|
||||
isolated = filter.send(:sanitizer)
|
||||
assert_not_same ActionText::ContentHelper.sanitizer, isolated,
|
||||
"must not reuse the process-wide ActionText sanitizer, whose permit scrubber is mutated per call"
|
||||
assert_not_same isolated, filter.send(:sanitizer),
|
||||
"each call must build a fresh sanitizer so concurrent requests never share scrubber state"
|
||||
assert_kind_of ActionText::ContentHelper.sanitizer.class, isolated
|
||||
end
|
||||
|
||||
test "SanitizeAttributes neutralizes unsafe input and preserves benign content on its isolated sanitizer" do
|
||||
body = <<~HTML.squish
|
||||
<div><a href="javascript:alert(1)" onclick="x()">link</a>
|
||||
<a href="data:text/html,pwned">data</a>
|
||||
<span class="cf-twitter-avatar" onmouseover="y()">avatar</span>
|
||||
<img src="https://evil.example/x.svg"> Hey #{mention_attachment_for(:david)}</div>
|
||||
HTML
|
||||
message = Message.create! room: rooms(:pets), body: body, client_message_id: "0015", creator: users(:jason)
|
||||
|
||||
filtered = ContentFilters::SanitizeAttributes.apply(message.body.body).to_html
|
||||
|
||||
assert_no_match /javascript:/, filtered
|
||||
assert_no_match /data:text\/html/, filtered
|
||||
assert_no_match /onclick/, filtered
|
||||
assert_no_match /onmouseover/, filtered
|
||||
assert_no_match /evil\.example/, filtered
|
||||
assert_match /<span class="cf-twitter-avatar">avatar<\/span>/, filtered
|
||||
assert_match />link</, filtered
|
||||
assert_match /<action-text-attachment sgid="#{users(:david).attachable_sgid}"/, filtered
|
||||
end
|
||||
|
||||
test "message with a mention attachment" do
|
||||
message = Message.create! room: rooms(:pets), body: "<div>Hey #{mention_attachment_for(:david)}</div>", creator: users(:jason)
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
require "test_helper"
|
||||
|
||||
class MessagesHelperTest < ActionView::TestCase
|
||||
test "message_presentation neutralizes unsafe URI schemes in links" do
|
||||
message = Message.create! room: rooms(:pets), body: '<div><a href="javascript:alert(1)">x</a></div>', client_message_id: "0015", creator: users(:jason)
|
||||
|
||||
presentation = view.message_presentation(message)
|
||||
assert_no_match /javascript:/, presentation
|
||||
assert_match /<a>x<\/a>/, presentation
|
||||
end
|
||||
|
||||
test "message_presentation strips event handler attributes from allowed tags" do
|
||||
message = Message.create! room: rooms(:pets), body: '<div><a href="/x" onmouseover="alert(1)">x</a></div>', client_message_id: "0015", creator: users(:jason)
|
||||
|
||||
presentation = view.message_presentation(message)
|
||||
assert_no_match /onmouseover/, presentation
|
||||
assert_match /<a href="\/x">x<\/a>/, presentation
|
||||
end
|
||||
|
||||
test "message_presentation preserves safe links and formatting" do
|
||||
message = Message.create! room: rooms(:pets), body: '<div><a href="https://example.com">example</a> <strong>bold</strong></div>', client_message_id: "0015", creator: users(:jason)
|
||||
|
||||
presentation = view.message_presentation(message)
|
||||
assert_match /<a href="https:\/\/example\.com"[^>]*>example<\/a>/, presentation
|
||||
assert_match /<strong>bold<\/strong>/, presentation
|
||||
end
|
||||
end
|
||||
@@ -1,128 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class CspNonceTest < ActionDispatch::IntegrationTest
|
||||
test "policy is delivered Report-Only, not enforced" do
|
||||
sign_in :david
|
||||
get root_url
|
||||
follow_redirect! # root redirects to the last room
|
||||
|
||||
assert_response :success
|
||||
assert response.headers["Content-Security-Policy-Report-Only"].present?,
|
||||
"Expected a Report-Only CSP header"
|
||||
assert_nil response.headers["Content-Security-Policy"],
|
||||
"Policy must not be enforced yet"
|
||||
end
|
||||
|
||||
test "nonce is stable across requests so Turbo restores don't trip CSP" do
|
||||
sign_in :david
|
||||
|
||||
get root_url
|
||||
nonce1 = report_only_nonce
|
||||
|
||||
get root_url
|
||||
nonce2 = report_only_nonce
|
||||
|
||||
assert nonce1.present?, "Expected a nonce in the Report-Only CSP header"
|
||||
assert_equal nonce1, nonce2, "Nonce must be stable across requests"
|
||||
end
|
||||
|
||||
test "client-set identifier still yields an unpredictable HMAC nonce" do
|
||||
fake_id = "attacker-controlled-value"
|
||||
cookies[CSP::Nonce::COOKIE] = fake_id
|
||||
|
||||
get root_url
|
||||
nonce = report_only_nonce
|
||||
|
||||
assert_equal CSP::Nonce.hmac(fake_id), nonce,
|
||||
"Nonce must be HMAC-SHA256 of the identifier keyed by secret_key_base"
|
||||
end
|
||||
|
||||
test "importmap script tag carries the nonce" do
|
||||
sign_in :david
|
||||
get root_url
|
||||
follow_redirect! # root redirects to the last room
|
||||
|
||||
assert_response :success
|
||||
nonce = report_only_nonce
|
||||
assert_select "script[type='importmap'][nonce=?]", nonce
|
||||
end
|
||||
|
||||
test "a per-install ENV extra host is appended to its directive" do
|
||||
with_env "CSP_EXTRA_FRAME_SRC" => "https://player.vimeo.com https://www.youtube.com",
|
||||
"CSP_EXTRA_IMG_SRC" => "https://cdn.example.test" do
|
||||
header = ActionDispatch::ContentSecurityPolicy.new { |p| CSP.apply(p) }.build
|
||||
|
||||
assert_match %r{frame-src[^;]*\bhttps://player\.vimeo\.com\b}, header
|
||||
assert_match %r{frame-src[^;]*\bhttps://www\.youtube\.com\b}, header
|
||||
assert_match %r{img-src[^;]*\bhttps://cdn\.example\.test\b}, header
|
||||
# :self is preserved alongside the extras.
|
||||
assert_match %r{frame-src 'self'}, header
|
||||
end
|
||||
end
|
||||
|
||||
test "a semicolon-separated ENV extra tokenizes into valid sources without raising" do
|
||||
# A plausible operator paste separates hosts with "; ". Because the nonce
|
||||
# forces a per-request policy build, a semicolon left inside a single source
|
||||
# token would make Rails raise InvalidDirectiveError on every request — a
|
||||
# site-wide 500 even in report-only mode. Splitting on ';' must yield both
|
||||
# hosts as valid tokens and never raise.
|
||||
with_env "CSP_EXTRA_FRAME_SRC" => "https://a.example; https://b.example" do
|
||||
header = nil
|
||||
assert_nothing_raised do
|
||||
header = ActionDispatch::ContentSecurityPolicy.new { |p| CSP.apply(p) }.build
|
||||
end
|
||||
|
||||
assert_match %r{frame-src[^;]*\bhttps://a\.example\b}, header
|
||||
assert_match %r{frame-src[^;]*\bhttps://b\.example\b}, header
|
||||
# Both hosts share the one frame-src directive; the semicolon did not leak
|
||||
# a second directive into the policy.
|
||||
assert_equal 1, header.scan(/(?:^|;\s*)frame-src\b/).size,
|
||||
"Expected exactly one frame-src directive"
|
||||
end
|
||||
end
|
||||
|
||||
test "public cacheable responses carry neither the policy nor the nonce cookie" do
|
||||
sign_in :david
|
||||
blob = ActiveStorage::Blob.create_and_upload!(io: file_fixture("moon.jpg").open, filename: "moon.jpg", content_type: "image/jpeg")
|
||||
get rails_blob_path(blob)
|
||||
disk_blob_url = response.location
|
||||
|
||||
[
|
||||
qr_code_path(Base64.urlsafe_encode64("http://example.com")),
|
||||
account_logo_path,
|
||||
user_avatar_path(users(:kevin).avatar_token),
|
||||
disk_blob_url
|
||||
].each do |path|
|
||||
cookies.delete CSP::Nonce::COOKIE
|
||||
|
||||
get path
|
||||
|
||||
assert_response :success
|
||||
assert response.cache_control[:public], "#{path} is expected to be publicly cacheable"
|
||||
assert_nil response.headers["Content-Security-Policy-Report-Only"], "#{path} must not carry the policy"
|
||||
assert_nil cookies[CSP::Nonce::COOKIE], "#{path} must not set the nonce cookie"
|
||||
end
|
||||
end
|
||||
|
||||
test "directives default to :self only when no ENV extras are set" do
|
||||
with_env "CSP_EXTRA_FRAME_SRC" => nil, "CSP_EXTRA_IMG_SRC" => nil do
|
||||
header = ActionDispatch::ContentSecurityPolicy.new { |p| CSP.apply(p) }.build
|
||||
|
||||
assert_match %r{frame-src 'self'(;|\z)}, header
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def with_env(vars)
|
||||
original = {}
|
||||
vars.each_key { |k| original[k] = ENV.key?(k) ? ENV[k] : :__unset__ }
|
||||
vars.each { |k, v| v.nil? ? ENV.delete(k) : ENV[k] = v }
|
||||
yield
|
||||
ensure
|
||||
original.each { |k, v| v == :__unset__ ? ENV.delete(k) : ENV[k] = v }
|
||||
end
|
||||
|
||||
def report_only_nonce
|
||||
response.headers["Content-Security-Policy-Report-Only"].to_s[/'nonce-([^']+)'/, 1]
|
||||
end
|
||||
end
|
||||
Vendored
-7
@@ -1,7 +0,0 @@
|
||||
/*
|
||||
Trix 2.1.19
|
||||
Copyright © 2026 37signals, LLC
|
||||
*/
|
||||
var t="2.1.19";const e="[data-trix-attachment]",i={preview:{presentation:"gallery",caption:{name:!0,size:!0}},file:{caption:{size:!0}}},n={default:{tagName:"div",parse:!1},quote:{tagName:"blockquote",nestable:!0},heading1:{tagName:"h1",terminal:!0,breakOnReturn:!0,group:!1},code:{tagName:"pre",terminal:!0,htmlAttributes:["language"],text:{plaintext:!0}},bulletList:{tagName:"ul",parse:!1},bullet:{tagName:"li",listAttribute:"bulletList",group:!1,nestable:!0,test(t){return r(t.parentNode)===n[this.listAttribute].tagName}},numberList:{tagName:"ol",parse:!1},number:{tagName:"li",listAttribute:"numberList",group:!1,nestable:!0,test(t){return r(t.parentNode)===n[this.listAttribute].tagName}},attachmentGallery:{tagName:"div",exclusive:!0,terminal:!0,parse:!1,group:!1}},r=t=>{var e;return null==t||null===(e=t.tagName)||void 0===e?void 0:e.toLowerCase()},o=navigator.userAgent.match(/android\s([0-9]+.*Chrome)/i),s=o&&parseInt(o[1]);var a={composesExistingText:/Android.*Chrome/.test(navigator.userAgent),recentAndroid:s&&s>12,samsungAndroid:s&&navigator.userAgent.match(/Android.*SM-/),forcesObjectResizing:/Trident.*rv:11/.test(navigator.userAgent),supportsInputEvents:"undefined"!=typeof InputEvent&&["data","getTargetRanges","inputType"].every((t=>t in InputEvent.prototype))},l={ADD_ATTR:["language"],SAFE_FOR_XML:!1,RETURN_DOM:!0},c={attachFiles:"Attach Files",bold:"Bold",bullets:"Bullets",byte:"Byte",bytes:"Bytes",captionPlaceholder:"Add a caption…",code:"Code",heading1:"Heading",indent:"Increase Level",italic:"Italic",link:"Link",numbers:"Numbers",outdent:"Decrease Level",quote:"Quote",redo:"Redo",remove:"Remove",strike:"Strikethrough",undo:"Undo",unlink:"Unlink",url:"URL",urlPlaceholder:"Enter a URL…",GB:"GB",KB:"KB",MB:"MB",PB:"PB",TB:"TB"};const u=[c.bytes,c.KB,c.MB,c.GB,c.TB,c.PB];var h={prefix:"IEC",precision:2,formatter(t){switch(t){case 0:return"0 ".concat(c.bytes);case 1:return"1 ".concat(c.byte);default:let e;"SI"===this.prefix?e=1e3:"IEC"===this.prefix&&(e=1024);const i=Math.floor(Math.log(t)/Math.log(e)),n=(t/Math.pow(e,i)).toFixed(this.precision).replace(/0*$/,"").replace(/\.$/,"");return"".concat(n," ").concat(u[i])}}};const d="\ufeff",g=" ",m=function(t){for(const e in t){const i=t[e];this[e]=i}return this},p=document.documentElement,f=p.matches,b=function(t){let{onElement:e,matchingSelector:i,withCallback:n,inPhase:r,preventDefault:o,times:s}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const a=e||p,l=i,c="capturing"===r,u=function(t){null!=s&&0==--s&&u.destroy();const e=x(t.target,{matchingSelector:l});null!=e&&(null==n||n.call(e,t,e),o&&t.preventDefault())};return u.destroy=()=>a.removeEventListener(t,u,c),a.addEventListener(t,u,c),u},v=function(t){let{bubbles:e,cancelable:i,attributes:n}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};e=!1!==e,i=!1!==i;const r=document.createEvent("Events");return r.initEvent(t,e,i),null!=n&&m.call(r,n),r},A=function(t){let{onElement:e,bubbles:i,cancelable:n,attributes:r}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const o=null!=e?e:p,s=v(t,{bubbles:i,cancelable:n,attributes:r});return o.dispatchEvent(s)},y=function(t,e){if(1===(null==t?void 0:t.nodeType))return f.call(t,e)},x=function(t){let{matchingSelector:e,untilNode:i}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};for(;t&&t.nodeType!==Node.ELEMENT_NODE;)t=t.parentNode;if(null!=t){if(null==e)return t;if(t.closest&&null==i)return t.closest(e);for(;t&&t!==i;){if(y(t,e))return t;t=t.parentNode}}},C=t=>document.activeElement!==t&&E(t,document.activeElement),E=function(t,e){if(t&&e)for(;e;){if(e===t)return!0;e=e.parentNode}},S=function(t){var e;if(null===(e=t)||void 0===e||!e.parentNode)return;let i=0;for(t=t.previousSibling;t;)i++,t=t.previousSibling;return i},R=t=>{var e;return null==t||null===(e=t.parentNode)||void 0===e?void 0:e.removeChild(t)},T=function(t){let{onlyNodesOfType:e,usingFilter:i,expandEntityReferences:n}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const r=(()=>{switch(e){case"element":return NodeFilter.SHOW_ELEMENT;case"text":return NodeFilter.SHOW_TEXT;case"comment":return NodeFilter.SHOW_COMMENT;default:return NodeFilter.SHOW_ALL}})();return document.createTreeWalker(t,r,null!=i?i:null,!0===n)},k=t=>{var e;return null==t||null===(e=t.tagName)||void 0===e?void 0:e.toLowerCase()},w=function(t){let e,i,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};"object"==typeof t?(n=t,t=n.tagName):n={attributes:n};const r=document.createElement(t);if(null!=n.editable&&(null==n.attributes&&(n.attributes={}),n.attributes.contenteditable=n.editable),n.attributes)for(e in n.attributes)i=n.attributes[e],r.setAttribute(e,i);if(n.style)for(e in n.style)i=n.style[e],r.style[e]=i;if(n.data)for(e in n.data)i=n.data[e],r.dataset[e]=i;return n.className&&n.className.split(" ").forEach((t=>{r.classList.add(t)})),n.textContent&&(r.textContent=n.textContent),n.childNodes&&[].concat(n.childNodes).forEach((t=>{r.appendChild(t)})),r};let D;const L=funLine truncated
|
||||
/*! @license DOMPurify 3.4.2 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.2/LICENSE */const{entries:oe,setPrototypeOf:se,isFrozen:ae,getPrototypeOf:le,getOwnPropertyDescriptor:ce}=Object;let{freeze:ue,seal:he,create:de}=Object,{apply:ge,construct:me}="undefined"!=typeof Reflect&&Reflect;ue||(ue=function(t){return t}),he||(he=function(t){return t}),ge||(ge=function(t,e){for(var i=arguments.length,n=new Array(i>2?i-2:0),r=2;r<i;r++)n[r-2]=arguments[r];return t.apply(e,n)}),me||(me=function(t){for(var e=arguments.length,i=new Array(e>1?e-1:0),n=1;n<e;n++)i[n-1]=arguments[n];return new t(...i)});const pe=Me(Array.prototype.forEach),fe=Me(Array.prototype.lastIndexOf),be=Me(Array.prototype.pop),ve=Me(Array.prototype.push),Ae=Me(Array.prototype.splice),ye=Array.isArray,xe=Me(String.prototype.toLowerCase),Ce=Me(String.prototype.toString),Ee=Me(String.prototype.match),Se=Me(String.prototype.replace),Re=Me(String.prototype.indexOf),Te=Me(String.prototype.trim),ke=Me(Number.prototype.toString),we=Me(Boolean.prototype.toString),De="undefined"==typeof BigInt?null:Me(BigInt.prototype.toString),Le="undefined"==typeof Symbol?null:Me(Symbol.prototype.toString),Ne=Me(Object.prototype.hasOwnProperty),Ie=Me(Object.prototype.toString),Oe=Me(RegExp.prototype.test),Fe=(Pe=TypeError,function(){for(var t=arguments.length,e=new Array(t),i=0;i<t;i++)e[i]=arguments[i];return me(Pe,e)});var Pe;function Me(t){return function(e){e instanceof RegExp&&(e.lastIndex=0);for(var i=arguments.length,n=new Array(i>1?i-1:0),r=1;r<i;r++)n[r-1]=arguments[r];return ge(t,e,n)}}function Be(t,e){let i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:xe;if(se&&se(t,null),!ye(e))return t;let n=e.length;for(;n--;){let r=e[n];if("string"==typeof r){const t=i(r);t!==r&&(ae(e)||(e[n]=t),r=t)}t[r]=!0}return t}function _e(t){for(let e=0;e<t.length;e++){Ne(t,e)||(t[e]=null)}return t}function je(t){const e=de(null);for(const[i,n]of oe(t)){Ne(t,i)&&(ye(n)?e[i]=_e(n):n&&"object"==typeof n&&n.constructor===Object?e[i]=je(n):e[i]=n)}return e}function We(t,e){for(;null!==t;){const i=ce(t,e);if(i){if(i.get)return Me(i.get);if("function"==typeof i.value)return Me(i.value)}t=le(t)}return function(){return null}}const Ue=ue(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),Ve=ue(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),ze=ue(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),He=ue(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),qe=ue(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),Je=ue(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),Ke=ue(["#text"]),Ge=ue(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","contLine truncated
|
||||
//# sourceMappingURL=trix.esm.min.js.map
|
||||
Reference in new issue
Block a user