Skip to content

Commit 8ed486d

Browse files
authored
Merge pull request #200 from flavorjones/199-css-functions-not-handled-properly
fix: handle CSS functions in a CSS shorthand property
2 parents fbe0846 + bf13d48 commit 8ed486d

File tree

5 files changed

+77
-39
lines changed

5 files changed

+77
-39
lines changed

lib/loofah/html5/scrub.rb

+40-23
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,22 @@ module HTML5 # :nodoc:
77
module Scrub
88
CONTROL_CHARACTERS = /[`\u0000-\u0020\u007f\u0080-\u0101]/
99
CSS_KEYWORDISH = /\A(#[0-9a-fA-F]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|-?\d{0,3}\.?\d{0,10}(ch|cm|r?em|ex|in|lh|mm|pc|pt|px|Q|vmax|vmin|vw|vh|%|,|\))?)\z/
10-
CRASS_SEMICOLON = { :node => :semicolon, :raw => ";" }
10+
CRASS_SEMICOLON = { node: :semicolon, raw: ";" }
1111
CSS_IMPORTANT = '!important'
1212

1313
class << self
1414
def allowed_element?(element_name)
15-
::Loofah::HTML5::SafeList::ALLOWED_ELEMENTS_WITH_LIBXML2.include? element_name
15+
::Loofah::HTML5::SafeList::ALLOWED_ELEMENTS_WITH_LIBXML2.include?(element_name)
1616
end
1717

1818
# alternative implementation of the html5lib attribute scrubbing algorithm
1919
def scrub_attributes(node)
2020
node.attribute_nodes.each do |attr_node|
2121
attr_name = if attr_node.namespace
22-
"#{attr_node.namespace.prefix}:#{attr_node.node_name}"
23-
else
24-
attr_node.node_name
25-
end
22+
"#{attr_node.namespace.prefix}:#{attr_node.node_name}"
23+
else
24+
attr_node.node_name
25+
end
2626

2727
if attr_name =~ /\Adata-[\w-]+\z/
2828
next
@@ -58,13 +58,13 @@ def scrub_attributes(node)
5858
end
5959
end
6060

61-
scrub_css_attribute node
61+
scrub_css_attribute(node)
6262

6363
node.attribute_nodes.each do |attr_node|
6464
node.remove_attribute(attr_node.name) if attr_node.value !~ /[^[:space:]]/
6565
end
6666

67-
force_correct_attribute_escaping! node
67+
force_correct_attribute_escaping!(node)
6868
end
6969

7070
def scrub_css_attribute(node)
@@ -73,33 +73,50 @@ def scrub_css_attribute(node)
7373
end
7474

7575
def scrub_css(style)
76-
style_tree = Crass.parse_properties style
76+
style_tree = Crass.parse_properties(style)
7777
sanitized_tree = []
7878

7979
style_tree.each do |node|
8080
next unless node[:node] == :property
8181
next if node[:children].any? do |child|
82-
[:url, :bad_url].include?(child[:node]) || (child[:node] == :function && !SafeList::ALLOWED_CSS_FUNCTIONS.include?(child[:name].downcase))
82+
[:url, :bad_url].include?(child[:node])
8383
end
84+
8485
name = node[:name].downcase
85-
if SafeList::ALLOWED_CSS_PROPERTIES.include?(name) || SafeList::ALLOWED_SVG_PROPERTIES.include?(name)
86-
sanitized_tree << node << CRASS_SEMICOLON
87-
elsif SafeList::SHORTHAND_CSS_PROPERTIES.include?(name.split("-").first)
88-
value = node[:value].split.map do |keyword|
89-
if SafeList::ALLOWED_CSS_KEYWORDS.include?(keyword) || keyword =~ CSS_KEYWORDISH
86+
next unless SafeList::ALLOWED_CSS_PROPERTIES.include?(name) ||
87+
SafeList::ALLOWED_SVG_PROPERTIES.include?(name) ||
88+
SafeList::SHORTHAND_CSS_PROPERTIES.include?(name.split("-").first)
89+
90+
value = node[:children].map do |child|
91+
case child[:node]
92+
when :whitespace
93+
nil
94+
when :string
95+
nil
96+
when :function
97+
if SafeList::ALLOWED_CSS_FUNCTIONS.include?(child[:name].downcase)
98+
Crass::Parser.stringify(child)
99+
end
100+
when :ident
101+
keyword = child[:value]
102+
if !SafeList::SHORTHAND_CSS_PROPERTIES.include?(name.split("-").first) ||
103+
SafeList::ALLOWED_CSS_KEYWORDS.include?(keyword) ||
104+
(keyword =~ CSS_KEYWORDISH)
90105
keyword
91106
end
92-
end.compact
93-
unless value.empty?
94-
value << CSS_IMPORTANT if node[:important]
95-
propstring = sprintf "%s:%s", name, value.join(" ")
96-
sanitized_node = Crass.parse_properties(propstring).first
97-
sanitized_tree << sanitized_node << CRASS_SEMICOLON
107+
else
108+
child[:raw]
98109
end
99-
end
110+
end.compact
111+
112+
next if value.empty?
113+
value << CSS_IMPORTANT if node[:important]
114+
propstring = format("%s:%s", name, value.join(" "))
115+
sanitized_node = Crass.parse_properties(propstring).first
116+
sanitized_tree << sanitized_node << CRASS_SEMICOLON
100117
end
101118

102-
Crass::Parser.stringify sanitized_tree
119+
Crass::Parser.stringify(sanitized_tree)
103120
end
104121

105122
#

test/assets/testdata_sanitizer_tests1.dat

+4-4
Original file line numberDiff line numberDiff line change
@@ -458,31 +458,31 @@
458458
"name": "style_attr_end_with_nothing",
459459
"input": "<div style=\"color: blue\" />",
460460
"output": "<div style='color: blue;'/>",
461-
"xhtml": "<div style='color: blue;'></div>",
461+
"xhtml": "<div style='color:blue;'></div>",
462462
"rexml": "<div style='color: blue;'></div>"
463463
},
464464

465465
{
466466
"name": "style_attr_end_with_space",
467467
"input": "<div style=\"color: blue \" />",
468468
"output": "<div style='color: blue ;'/>",
469-
"xhtml": "<div style='color: blue ;'></div>",
469+
"xhtml": "<div style='color:blue;'></div>",
470470
"rexml": "<div style='color: blue ;'></div>"
471471
},
472472

473473
{
474474
"name": "style_attr_end_with_semicolon",
475475
"input": "<div style=\"color: blue;\" />",
476476
"output": "<div style='color: blue;'/>",
477-
"xhtml": "<div style='color: blue;'></div>",
477+
"xhtml": "<div style='color:blue;'></div>",
478478
"rexml": "<div style='color: blue;'></div>"
479479
},
480480

481481
{
482482
"name": "style_attr_end_with_semicolon_space",
483483
"input": "<div style=\"color: blue; \" />",
484484
"output": "<div style='color: blue;'/>",
485-
"xhtml": "<div style='color: blue;'></div>",
485+
"xhtml": "<div style='color:blue;'></div>",
486486
"rexml": "<div style='color: blue;'></div>"
487487
},
488488

test/html5/test_scrub.rb

-10
This file was deleted.

test/html5/test_scrub_css.rb

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# frozen_string_literal: true
2+
require "helper"
3+
4+
class UnitHTML5Scrub < Loofah::TestCase
5+
include Loofah
6+
7+
describe "hex values" do
8+
it "handles upper case" do
9+
assert_equal "background:#ABC012;", Loofah::HTML5::Scrub.scrub_css("background: #ABC012")
10+
end
11+
it "handles lower case" do
12+
assert_equal "background:#abc012;", Loofah::HTML5::Scrub.scrub_css("background: #abc012")
13+
end
14+
end
15+
16+
describe "css functions" do
17+
it "allows safe functions" do
18+
assert_equal "background-color:linear-gradient(transparent 50%, #ffff66 50%);",
19+
Loofah::HTML5::Scrub.scrub_css("background-color: linear-gradient(transparent 50%, #ffff66 50%);")
20+
end
21+
22+
it "disallows unsafe functions" do
23+
assert_equal "", Loofah::HTML5::Scrub.scrub_css("background-color: haxxor-fun(transparent 50%, #ffff66 50%);")
24+
end
25+
26+
# see #199 for the bug we're testing here
27+
it "allows safe functions in shorthand css properties" do
28+
assert_equal "background:linear-gradient(transparent 50%, #ffff66 50%);",
29+
Loofah::HTML5::Scrub.scrub_css("background: linear-gradient(transparent 50%, #ffff66 50%);")
30+
end
31+
end
32+
end

test/integration/test_ad_hoc.rb

+1-2
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,7 @@ def test_dont_remove_whitespace_between_tags
226226

227227
it "Loofah.document removes the comment" do
228228
sanitized = Loofah.document(html)
229-
sanitized_html = sanitized.to_html
230-
refute_match(/--/, sanitized_html)
229+
refute(sanitized.children.any? { |node| node.comment? } )
231230
end
232231

233232
it "Loofah.scrub_document removes the comment" do

0 commit comments

Comments
 (0)