Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions app/helpers/better_together/content/blocks_helper.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
# frozen_string_literal: true

require 'css_parser'

module BetterTogether
module Content
# Helpers for Content Blocks
module BlocksHelper
ALLOWED_CSS_PROPERTIES = %w[
align-items aspect-ratio background background-color border border-color border-radius
border-style border-width box-shadow color content display flex flex-direction font-family
font-size font-weight height justify-content left line-height margin margin-bottom margin-left
margin-right margin-top max-height max-width min-height min-width object-fit overflow
overflow-y padding padding-bottom padding-left padding-right padding-top position text-align
text-decoration text-shadow width z-index
].freeze

# Returns an array of acceptable image file types
def acceptable_image_file_types
BetterTogether::Attachments::Images::VALID_IMAGE_CONTENT_TYPES
Expand All @@ -14,6 +25,42 @@ def temp_id_for(model, temp_id: SecureRandom.uuid)
model.persisted? ? model.id : temp_id
end

# Sanitize css content by only allowing whitelisted properties
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
def sanitize_css(content)
return if content.blank?

parser = CssParser::Parser.new
parser.add_block!(content)
safe_css = ''

parser.each_selector do |selector, declarations, _specificity, media_types|
safe_decls = declarations.split(';').filter_map do |decl|
raw_property, value = decl.split(':', 2)
next unless raw_property && value

property = raw_property.strip
normalized = property.downcase
value = value.strip
next unless ALLOWED_CSS_PROPERTIES.include?(normalized) || property.start_with?('--')

"#{property}: #{value}"
end
next if safe_decls.empty?

if media_types&.any?
safe_css << "@media #{media_types.join(', ')} { #{selector} { #{safe_decls.join('; ')} } }\n"
else
safe_css << "#{selector} { #{safe_decls.join('; ')} }\n"
end
end
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize

safe_css.presence
rescue CssParser::ParserError
nil
end

# Sanitize HTML content for safe rendering in custom blocks
def sanitize_block_html(html)
allowed_tags = %w[p br strong em b i ul ol li a span h1 h2 h3 h4 h5 h6 img figure figcaption blockquote pre
Expand Down
5 changes: 4 additions & 1 deletion app/views/better_together/content/blocks/_css.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@

<% if css.content.present? %>
<%= content_tag :style, sanitize_block_css(css.content), type: 'text/css', id: dom_id(css) %>
<% sanitized = sanitize_css(css.content) %>
<% if sanitized.present? %>
<%= content_tag :style, sanitized.html_safe, type: 'text/css', id: dom_id(css) %>
<% end %>
<% end %>
55 changes: 55 additions & 0 deletions spec/views/better_together/content/blocks/css.html.erb_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe 'better_together/content/blocks/css', type: :view do
it 'renders whitelisted properties' do
css = BetterTogether::Content::Css.new(content: 'h1 { color: red; position: relative; z-index: 1; }')

render partial: 'better_together/content/blocks/css', locals: { css: css }

expect(rendered).to include('color: red')
expect(rendered).to include('position: relative')
expect(rendered).to include('z-index: 1')
expect(rendered).not_to include('<style></style>')
end

it 'strips disallowed properties' do
css = BetterTogether::Content::Css.new(content: 'h1 { color: red; float: left; }')

render partial: 'better_together/content/blocks/css', locals: { css: css }

expect(rendered).to include('color: red')
expect(rendered).not_to include('float: left')
end

it 'rejects block when all rules disallowed' do
css = BetterTogether::Content::Css.new(content: 'h1 { float: left; }')

render partial: 'better_together/content/blocks/css', locals: { css: css }

expect(rendered.strip).to be_empty
end

it 'preserves media queries while filtering properties' do
css = BetterTogether::Content::Css.new(
content: '@media only screen and (min-width: 768px) { h1 { font-size: 3em; float: left; } }'
)

render partial: 'better_together/content/blocks/css', locals: { css: css }

expect(rendered).to include('@media only screen and (min-width: 768px)')
expect(rendered).to include('font-size: 3em')
expect(rendered).not_to include('float: left')
end

it 'allows custom properties' do
css = BetterTogether::Content::Css.new(
content: '.navbar { --bs-navbar-toggler-padding-x: 0.25rem; }'
)

render partial: 'better_together/content/blocks/css', locals: { css: css }

expect(rendered).to include('--bs-navbar-toggler-padding-x: 0.25rem')
end
end
Loading