loofah 2.25.1 → 2.25.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -0
- data/lib/loofah/html5/safelist.rb +8 -3
- data/lib/loofah/html5/scrub.rb +175 -43
- data/lib/loofah/scrubbers.rb +22 -21
- data/lib/loofah/version.rb +1 -1
- data/lib/loofah.rb +1 -7
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ab3a16b00807f63098be8c67dbd785edea496daf292dc063299d84d25ae56133
|
|
4
|
+
data.tar.gz: d9ba643579b0190c1f66aa4cdfeb79ec5ade635032172c805be58bd0d67fc5fc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b9b7dc5b9f7e3c0ac798a7e70f6257bb65827baadae3861d17cf0a24e1bf5cab48c909cd463ecc51ae5ba59acd91ca73c004faa7d1c489bbd0e91b7245132c79
|
|
7
|
+
data.tar.gz: 7e17e0f6e9cec7edd72c0f254aa406e510531297af9acc10714cd7277ec1641d48ab369a0bfeeef585104c314a79cdfb795f112e84591a5cf81f4cb8eac46486
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.25.3 / 2026-11-01
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
|
|
7
|
+
* Ensure `Loofah::HTML5::Scrub.allowed_uri?` rejects schemes split by a numeric character reference without a semicolon that ends at a character `allowed_uri?` strips (e.g. `javascript: alert(1)`) or at another character reference (e.g. `javascript:alert(1)`). Character references are now decoded before anything is stripped, as browsers do. See [GHSA-p9wj-w8xq-vjx6](https://github.com/flavorjones/loofah/security/advisories/GHSA-p9wj-w8xq-vjx6). @flavorjones
|
|
8
|
+
* Ensure `Loofah::HTML5::Scrub.allowed_uri?` recognizes the named character reference `/`, which `CGI.unescapeHTML` does not decode and browsers read as `/`, and rejects `data:` URIs that disguise a disallowed mediatype with it (e.g. `data:text/html,<script>alert(1)</script>`). Previously that mediatype was treated as malformed and fell back to the allowed `text/plain`. Only v2.25.2 is affected. See [GHSA-jcff-cj73-4w4f](https://github.com/flavorjones/loofah/security/advisories/GHSA-jcff-cj73-4w4f). @flavorjones
|
|
9
|
+
* Ensure `Loofah::HTML5::Scrub.scrub_css` does not decode CSS escapes in identifiers. Previously, escaped parentheses (e.g. `url\28 http://example.com/\29`) came out as a live `url()`, which could make the viewer's browser fetch an attacker-chosen URL. See [GHSA-m3x9-938q-5767](https://github.com/flavorjones/loofah/security/advisories/GHSA-m3x9-938q-5767). @flavorjones
|
|
10
|
+
* Ensure that `href` and `xlink:href` on SVG `textPath` and `mpath` elements are restricted to local (same-document) references. Previously `SVG_ALLOW_LOCAL_HREF` spelled `textPath` in lowercase, which never matched the name the HTML5 parser assigns inside `<svg>`, and omitted `mpath` entirely. See [GHSA-345x-3rq2-jfrm](https://github.com/flavorjones/loofah/security/advisories/GHSA-345x-3rq2-jfrm). @flavorjones
|
|
11
|
+
* Fix quadratic-time scrubbing of the `href` and `xlink:href` attributes of SVG elements. Only Ruby 3.1 and earlier are affected, because Ruby 3.2 introduced linear-time regexp matching. See [GHSA-mf2j-8f84-cc9q](https://github.com/flavorjones/loofah/security/advisories/GHSA-mf2j-8f84-cc9q). @flavorjones
|
|
12
|
+
* Ensure HTML5 scrubber output does not turn into new markup when a browser parses it again. The fix is in Nokogiri v1.19.5, which escapes `<` and `>` in attribute values as the HTML standard requires, so Loofah now requires Nokogiri >= 1.19.5. See [GHSA-h6v6-rg66-fqj4](https://github.com/flavorjones/loofah/security/advisories/GHSA-h6v6-rg66-fqj4). @flavorjones @hackvertor
|
|
13
|
+
|
|
14
|
+
### Dependencies
|
|
15
|
+
|
|
16
|
+
* Loofah now requires Ruby >= 3.2, the minimum that Nokogiri v1.19.x supports. We don't like dropping Ruby versions in a patch release, and we're sorry for the inconvenience, but it's the only way to make sure everyone gets the security fix in [GHSA-h6v6-rg66-fqj4](https://github.com/flavorjones/loofah/security/advisories/GHSA-h6v6-rg66-fqj4).
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
* `Loofah::HTML5::Scrub.prefixed_attribute_name` and `Loofah::HTML5::Scrub.scrub_attribute_value` expose the html5lib attribute name and value rules, so downstream consumers can apply them instead of carrying their own copy. @flavorjones
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
* Fix `:double_breakpoint`, which left document fragments unchanged despite its documented example. The scrubber now changes only paragraphs inside the scrubbed node, and its run time on a document grows linearly, not quadratically, with the paragraph count. #309 @OskarEichler
|
|
25
|
+
* Fix `:whitewash`, which passed CDATA sections through unchanged. Markup inside a CDATA section in `Loofah.xml_fragment` or `Loofah.xml_document` survived scrubbing and became live elements if the output was later parsed as HTML. The scrubber now escapes CDATA content, as `:strip`, `:prune` and `:escape` already do. On JRuby, every sanitizer now emits escaped CDATA content as a CDATA section, as on CRuby, instead of a text node that double-escaped it. #313 @flavorjones (reported by @MegaManSec)
|
|
26
|
+
* Fix `:double_breakpoint`, which raised `NoMethodError` on a paragraph that ends with a `<br>` after a double break. #314 @flavorjones (reported by @MegaManSec)
|
|
27
|
+
* Fix `Loofah::HTML5::Scrub.allowed_uri?`, which raised `Encoding::CompatibilityError` on a string in an encoding other than UTF-8 and `ArgumentError` on invalid UTF-8. It now transcodes the input to UTF-8 and returns `false` when the input has an invalid byte sequence or no UTF-8 representation. #315 @flavorjones
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## 2.25.2 / 2026-07-15
|
|
31
|
+
|
|
32
|
+
### Security
|
|
33
|
+
|
|
34
|
+
* Ensure `Loofah::HTML5::Scrub.allowed_uri?` recognizes numeric character references without semicolons (e.g. `javascript:alert(1)`), which browsers decode and execute, and rejects schemes split by them. See [GHSA-5qhf-9phg-95m2](https://github.com/flavorjones/loofah/security/advisories/GHSA-5qhf-9phg-95m2). @flavorjones
|
|
35
|
+
* Ensure `Loofah::HTML5::Scrub.allowed_uri?` recognizes the named character references `	` and `
`, which `CGI.unescapeHTML` does not decode and browsers strip from URIs, and rejects schemes split by them (e.g. `java	script:alert(1)`). See [GHSA-8whx-365g-h9vv](https://github.com/flavorjones/loofah/security/advisories/GHSA-8whx-365g-h9vv). @flavorjones
|
|
36
|
+
* Ensure that both `href` and `xlink:href` attributes on SVG elements like `use` are restricted to local (same-document) references. Previously only `xlink:href` was restricted, allowing the SVG 2 `href` attribute to reference external documents. See [GHSA-9wjq-cp2p-hrgf](https://github.com/flavorjones/loofah/security/advisories/GHSA-9wjq-cp2p-hrgf). @flavorjones
|
|
37
|
+
|
|
38
|
+
### Improved
|
|
39
|
+
|
|
40
|
+
* Harden `data:` URI mediatype parsing in `Loofah::HTML5::Scrub.allowed_uri?`. The mediatype is now parsed following the [WHATWG data: URL spec](https://fetch.spec.whatwg.org/#data-urls) and [RFC 2397](https://www.rfc-editor.org/rfc/rfc2397) instead of simply being split on a colon. A `data:` URI with an omitted or malformed mediatype is now treated as `text/plain` and allowed, and one without the required comma is now rejected. #305 @flavorjones
|
|
41
|
+
* Remove `feed` from the default set of allowed protocols. The [feed URI scheme](https://en.wikipedia.org/wiki/Feed_URI_scheme) was never accepted as a standard protocol, and no major browser supports it. Removing it reduces the attack surface particularly for non-browser contexts. #304 @flavorjones
|
|
42
|
+
* Remove a vestigial `p` alternative from `Loofah::HTML5::SafeList::PROTOCOL_SEPARATOR`. This appears to be an ancient typo dating back to pre-extraction Rails circa 2007. #305 @flavorjones
|
|
43
|
+
|
|
44
|
+
|
|
3
45
|
## 2.25.1 / 2026-03-17
|
|
4
46
|
|
|
5
47
|
* Ensure `Loofah::HTML5::Scrub.allowed_uri?` recognizes unescaped whitespace entities and rejects schemas containing them. See [GHSA-46fp-8f5p-pf2m](https://github.com/flavorjones/loofah/security/advisories/GHSA-46fp-8f5p-pf2m). #302 @flavorjones
|
|
@@ -605,6 +605,11 @@ module Loofah
|
|
|
605
605
|
"stroke",
|
|
606
606
|
])
|
|
607
607
|
|
|
608
|
+
SVG_HREF_ATTRIBUTES = Set.new([
|
|
609
|
+
"xlink:href",
|
|
610
|
+
"href",
|
|
611
|
+
])
|
|
612
|
+
|
|
608
613
|
SVG_ALLOW_LOCAL_HREF = Set.new([
|
|
609
614
|
"altGlyph",
|
|
610
615
|
"animate",
|
|
@@ -618,7 +623,8 @@ module Loofah
|
|
|
618
623
|
"pattern",
|
|
619
624
|
"radialGradient",
|
|
620
625
|
"set",
|
|
621
|
-
"
|
|
626
|
+
"mpath",
|
|
627
|
+
"textPath",
|
|
622
628
|
"tref",
|
|
623
629
|
"use",
|
|
624
630
|
])
|
|
@@ -978,7 +984,7 @@ module Loofah
|
|
|
978
984
|
"stroke-opacity",
|
|
979
985
|
])
|
|
980
986
|
|
|
981
|
-
PROTOCOL_SEPARATOR = /:|(�*58)|(&#
|
|
987
|
+
PROTOCOL_SEPARATOR = /:|(�*58)|(�*3a)|(%|%)3A/i
|
|
982
988
|
|
|
983
989
|
ACCEPTABLE_PROTOCOLS = Set.new([
|
|
984
990
|
"afs",
|
|
@@ -987,7 +993,6 @@ module Loofah
|
|
|
987
993
|
"data",
|
|
988
994
|
"ed2k",
|
|
989
995
|
"fax",
|
|
990
|
-
"feed",
|
|
991
996
|
"ftp",
|
|
992
997
|
"gopher",
|
|
993
998
|
"http",
|
data/lib/loofah/html5/scrub.rb
CHANGED
|
@@ -13,8 +13,54 @@ module Loofah
|
|
|
13
13
|
CSS_IMPORTANT = "!important"
|
|
14
14
|
CSS_WHITESPACE = " "
|
|
15
15
|
CSS_PROPERTY_STRING_WITHOUT_EMBEDDED_QUOTES = /\A(["'])?[^"']+\1\z/
|
|
16
|
+
CSS_URLISH_NODES = [:url, :bad_url, :bad_string]
|
|
16
17
|
DATA_ATTRIBUTE_NAME = /\Adata-[\w-]+\z/
|
|
17
|
-
|
|
18
|
+
|
|
19
|
+
# Decimal (`:`) or hexadecimal (`:`) form, with or without the trailing semicolon that
|
|
20
|
+
# CGI.unescapeHTML requires but browsers do not.
|
|
21
|
+
NUMERIC_CHARACTER_REFERENCE = /&#(x[0-9a-f]+|[0-9]+);?/i
|
|
22
|
+
|
|
23
|
+
# A scheme (RFC 3986) followed by a protocol separator. The separator must recognize the same
|
|
24
|
+
# encoded-colon forms as PROTOCOL_SEPARATOR, otherwise a scheme split by an encoded colon (for
|
|
25
|
+
# example "javascript:alert(1)") would not be recognized as having a scheme and would skip
|
|
26
|
+
# protocol validation.
|
|
27
|
+
URI_PROTOCOL_REGEX = /\A[a-z][a-z0-9+\-.]*#{SafeList::PROTOCOL_SEPARATOR}/
|
|
28
|
+
|
|
29
|
+
# Matches a valid MIME type "essence" (type "/" subtype, no parameters), used to
|
|
30
|
+
# decide whether a data: URI mediatype is well-formed; a non-match is not a valid
|
|
31
|
+
# MIME type, which the data: URL processor treats as text/plain. Specs:
|
|
32
|
+
#
|
|
33
|
+
# https://mimesniff.spec.whatwg.org/#valid-mime-type
|
|
34
|
+
# https://mimesniff.spec.whatwg.org/#mime-type-essence
|
|
35
|
+
# https://mimesniff.spec.whatwg.org/#http-token-code-point
|
|
36
|
+
#
|
|
37
|
+
# The character class below is the HTTP token set (tchar) from RFC 9110 section
|
|
38
|
+
# 5.6.2, https://www.rfc-editor.org/rfc/rfc9110#name-tokens :
|
|
39
|
+
#
|
|
40
|
+
# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^"
|
|
41
|
+
# / "_" / "`" / "|" / "~" / DIGIT / ALPHA
|
|
42
|
+
#
|
|
43
|
+
# ALPHA is written a-z, not a-zA-Z, because allowed_uri? downcases the input first.
|
|
44
|
+
DATA_URI_MEDIATYPE = %r{
|
|
45
|
+
\A
|
|
46
|
+
[a-z0-9!\#$%&'*+\-.^_`|~]+ # type: 1*tchar
|
|
47
|
+
/ # "/" is not a tchar, so it is the sole delimiter
|
|
48
|
+
[a-z0-9!\#$%&'*+\-.^_`|~]+ # subtype: 1*tchar
|
|
49
|
+
\z
|
|
50
|
+
}x
|
|
51
|
+
|
|
52
|
+
# HTML5 named character references for whitespace that browsers strip from
|
|
53
|
+
# URIs. CGI.unescapeHTML does not decode these, so they are handled explicitly.
|
|
54
|
+
WHITESPACE_CHARACTER_REFERENCES = /&(Tab|NewLine);/
|
|
55
|
+
|
|
56
|
+
# HTML5 named character references for the delimiters that decide how a URI parses:
|
|
57
|
+
# ":" ends the scheme, and "/" separates the type and subtype of a data: mediatype.
|
|
58
|
+
# CGI.unescapeHTML does not decode these either. Together with
|
|
59
|
+
# WHITESPACE_CHARACTER_REFERENCES these are every named character reference that
|
|
60
|
+
# decodes to U+0009, U+000A, U+002F, or U+003A, so no other named reference can
|
|
61
|
+
# change how a URI parses.
|
|
62
|
+
DELIMITER_CHARACTER_REFERENCES = { ":" => ":", "/" => "/" }.freeze
|
|
63
|
+
DELIMITER_CHARACTER_REFERENCE = Regexp.union(DELIMITER_CHARACTER_REFERENCES.keys)
|
|
18
64
|
|
|
19
65
|
class << self
|
|
20
66
|
def allowed_element?(element_name)
|
|
@@ -24,11 +70,7 @@ module Loofah
|
|
|
24
70
|
# alternative implementation of the html5lib attribute scrubbing algorithm
|
|
25
71
|
def scrub_attributes(node)
|
|
26
72
|
node.attribute_nodes.each do |attr_node|
|
|
27
|
-
attr_name =
|
|
28
|
-
"#{attr_node.namespace.prefix}:#{attr_node.node_name}"
|
|
29
|
-
else
|
|
30
|
-
attr_node.node_name
|
|
31
|
-
end
|
|
73
|
+
attr_name = prefixed_attribute_name(attr_node)
|
|
32
74
|
|
|
33
75
|
if DATA_ATTRIBUTE_NAME.match?(attr_name)
|
|
34
76
|
next
|
|
@@ -39,20 +81,7 @@ module Loofah
|
|
|
39
81
|
next
|
|
40
82
|
end
|
|
41
83
|
|
|
42
|
-
|
|
43
|
-
next if scrub_uri_attribute(attr_node)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
if SafeList::SVG_ATTR_VAL_ALLOWS_REF.include?(attr_name)
|
|
47
|
-
scrub_attribute_that_allows_local_ref(attr_node)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
next unless SafeList::SVG_ALLOW_LOCAL_HREF.include?(node.name) &&
|
|
51
|
-
attr_name == "xlink:href" &&
|
|
52
|
-
attr_node.value =~ /^\s*[^#\s].*/m
|
|
53
|
-
|
|
54
|
-
attr_node.remove
|
|
55
|
-
next
|
|
84
|
+
scrub_attribute_value(node, attr_node, attr_name)
|
|
56
85
|
end
|
|
57
86
|
|
|
58
87
|
scrub_css_attribute(node)
|
|
@@ -66,28 +95,58 @@ module Loofah
|
|
|
66
95
|
force_correct_attribute_escaping!(node)
|
|
67
96
|
end
|
|
68
97
|
|
|
98
|
+
# The name an attribute is matched by in the safelists, qualified by its namespace prefix.
|
|
99
|
+
def prefixed_attribute_name(attr_node)
|
|
100
|
+
if attr_node.namespace
|
|
101
|
+
"#{attr_node.namespace.prefix}:#{attr_node.node_name}"
|
|
102
|
+
else
|
|
103
|
+
attr_node.node_name
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Apply the html5lib value rules to an attribute that is already permitted by name.
|
|
108
|
+
# Returns true when `scrub_uri_attribute` removed the attribute, in which case the caller
|
|
109
|
+
# should stop scrubbing it.
|
|
110
|
+
def scrub_attribute_value(node, attr_node, attr_name = prefixed_attribute_name(attr_node))
|
|
111
|
+
if SafeList::ATTR_VAL_IS_URI.include?(attr_name)
|
|
112
|
+
return true if scrub_uri_attribute(attr_node)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
if SafeList::SVG_ATTR_VAL_ALLOWS_REF.include?(attr_name)
|
|
116
|
+
scrub_attribute_that_allows_local_ref(attr_node)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The whitespace run is `[^\S\n]*` rather than `\s*` so that it cannot cross a line
|
|
120
|
+
# boundary. With `\s*` the engine rescanned the entire run from every line start,
|
|
121
|
+
# which is quadratic in the length of an all-whitespace value.
|
|
122
|
+
if SafeList::SVG_ALLOW_LOCAL_HREF.include?(node.name) &&
|
|
123
|
+
SafeList::SVG_HREF_ATTRIBUTES.include?(attr_name) &&
|
|
124
|
+
attr_node.value =~ /^[^\S\n]*[^#\s]/
|
|
125
|
+
attr_node.remove
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
false
|
|
129
|
+
end
|
|
130
|
+
|
|
69
131
|
def scrub_css_attribute(node)
|
|
70
132
|
style = node.attributes["style"]
|
|
71
133
|
style.value = scrub_css(style.value) if style
|
|
72
134
|
end
|
|
73
135
|
|
|
74
136
|
def scrub_css(style)
|
|
75
|
-
url_flags = [:url, :bad_url]
|
|
76
137
|
style_tree = Crass.parse_properties(style)
|
|
77
138
|
sanitized_tree = []
|
|
78
139
|
|
|
79
140
|
style_tree.each do |node|
|
|
80
141
|
next unless node[:node] == :property
|
|
81
|
-
next if node[:children].any?
|
|
82
|
-
url_flags.include?(child[:node])
|
|
83
|
-
end
|
|
142
|
+
next if node[:children].any? { |child| CSS_URLISH_NODES.include?(child[:node]) }
|
|
84
143
|
|
|
85
144
|
name = node[:name].downcase
|
|
86
145
|
next unless SafeList::ALLOWED_CSS_PROPERTIES.include?(name) ||
|
|
87
146
|
SafeList::ALLOWED_SVG_PROPERTIES.include?(name) ||
|
|
88
147
|
SafeList::SHORTHAND_CSS_PROPERTIES.include?(name.split("-").first)
|
|
89
148
|
|
|
90
|
-
value = node[:children].
|
|
149
|
+
value = node[:children].filter_map do |child|
|
|
91
150
|
case child[:node]
|
|
92
151
|
when :whitespace
|
|
93
152
|
CSS_WHITESPACE
|
|
@@ -104,30 +163,55 @@ module Loofah
|
|
|
104
163
|
if !SafeList::SHORTHAND_CSS_PROPERTIES.include?(name.split("-").first) ||
|
|
105
164
|
SafeList::ALLOWED_CSS_KEYWORDS.include?(keyword) ||
|
|
106
165
|
(keyword =~ CSS_KEYWORDISH)
|
|
107
|
-
|
|
166
|
+
child[:raw]
|
|
108
167
|
end
|
|
109
168
|
else
|
|
110
169
|
child[:raw]
|
|
111
170
|
end
|
|
112
|
-
end.
|
|
171
|
+
end.join.strip
|
|
113
172
|
|
|
114
173
|
next if value.empty?
|
|
115
174
|
|
|
116
175
|
value << CSS_WHITESPACE << CSS_IMPORTANT if node[:important]
|
|
176
|
+
# Dropping tokens can join the survivors into new ones (`url/(a)*/(b)` becomes `url/*/`),
|
|
177
|
+
# so the rebuilt string may tokenize differently from the original and stringify
|
|
178
|
+
# differently again. Keep a property only if its rebuilt string survives a parse and
|
|
179
|
+
# stringify unchanged, so the tokens checked are exactly the tokens a browser will see.
|
|
117
180
|
propstring = format("%s:%s", name, value)
|
|
118
181
|
sanitized_node = Crass.parse_properties(propstring).first
|
|
182
|
+
next unless Crass::Parser.stringify(sanitized_node) == propstring
|
|
183
|
+
next if css_property_unsafe?(sanitized_node)
|
|
184
|
+
|
|
119
185
|
sanitized_tree << sanitized_node << CRASS_SEMICOLON
|
|
120
186
|
end
|
|
121
187
|
|
|
122
188
|
Crass::Parser.stringify(sanitized_tree)
|
|
123
189
|
end
|
|
124
190
|
|
|
191
|
+
# Dropping a token joins its neighbours, which can build a token that was not in the input:
|
|
192
|
+
# dropping the string from `url\28 "x"linear-gradient(red)` leaves one function token. So the
|
|
193
|
+
# rebuilt property is checked for functions and strings, which the input check never sees.
|
|
194
|
+
def css_property_unsafe?(node)
|
|
195
|
+
node[:children].any? do |child|
|
|
196
|
+
case child[:node]
|
|
197
|
+
when *CSS_URLISH_NODES
|
|
198
|
+
true
|
|
199
|
+
when :string
|
|
200
|
+
!CSS_PROPERTY_STRING_WITHOUT_EMBEDDED_QUOTES.match?(child[:raw])
|
|
201
|
+
when :function
|
|
202
|
+
!SafeList::ALLOWED_CSS_FUNCTIONS.include?(child[:name].downcase(:ascii))
|
|
203
|
+
else
|
|
204
|
+
false
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
125
209
|
def scrub_attribute_that_allows_local_ref(attr_node)
|
|
126
210
|
return unless attr_node.value
|
|
127
211
|
|
|
128
212
|
nodes = Crass::Parser.new(attr_node.value).parse_component_values
|
|
129
213
|
|
|
130
|
-
values = nodes.
|
|
214
|
+
values = nodes.filter_map do |node|
|
|
131
215
|
case node[:node]
|
|
132
216
|
when :url
|
|
133
217
|
if node[:value].start_with?("#")
|
|
@@ -136,34 +220,68 @@ module Loofah
|
|
|
136
220
|
when :hash, :ident, :string
|
|
137
221
|
node[:raw]
|
|
138
222
|
end
|
|
139
|
-
end
|
|
223
|
+
end
|
|
140
224
|
|
|
141
225
|
attr_node.value = values.join(" ")
|
|
142
226
|
end
|
|
143
227
|
|
|
144
|
-
# Returns true if the given URI string is safe, false otherwise.
|
|
145
|
-
#
|
|
146
|
-
# requiring a Nokogiri DOM node.
|
|
228
|
+
# Returns true if the given URI string is safe, false otherwise. This method can be used to
|
|
229
|
+
# validate URI attribute values without requiring a Nokogiri DOM node.
|
|
147
230
|
def allowed_uri?(uri_string)
|
|
148
|
-
#
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
231
|
+
# The character-reference regexps are UTF-8, so the input has to be transcoded before it can
|
|
232
|
+
# be matched against them. String#encode is a no-op on a string already tagged UTF-8, so an
|
|
233
|
+
# invalid byte sequence has to be rejected separately.
|
|
234
|
+
return false unless uri_string.valid_encoding?
|
|
235
|
+
|
|
236
|
+
begin
|
|
237
|
+
uri_string = uri_string.encode(Encoding::UTF_8)
|
|
238
|
+
rescue EncodingError
|
|
239
|
+
return false
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Decode character references before stripping anything, as a browser does: an HTML parser
|
|
243
|
+
# decodes the attribute value, then the URL parser strips characters from the result. A
|
|
244
|
+
# semicolon-less numeric reference ends at the first character that cannot extend it, so
|
|
245
|
+
# stripping that character first would let the reference run on into the following text.
|
|
246
|
+
# CGI.unescapeHTML decodes numeric references only when they carry a trailing semicolon, so
|
|
247
|
+
# the semicolon-less ones are decoded separately.
|
|
248
|
+
uri_string = CGI.unescapeHTML(decode_numeric_character_references(uri_string))
|
|
249
|
+
uri_string.gsub!(CONTROL_CHARACTERS, "")
|
|
250
|
+
uri_string.gsub!(WHITESPACE_CHARACTER_REFERENCES, "")
|
|
251
|
+
uri_string.gsub!(DELIMITER_CHARACTER_REFERENCE, DELIMITER_CHARACTER_REFERENCES)
|
|
252
|
+
uri_string.downcase!
|
|
153
253
|
if URI_PROTOCOL_REGEX.match?(uri_string)
|
|
154
254
|
protocol = uri_string.split(SafeList::PROTOCOL_SEPARATOR)[0]
|
|
155
255
|
return false unless SafeList::ALLOWED_PROTOCOLS.include?(protocol)
|
|
156
256
|
|
|
157
257
|
if protocol == "data"
|
|
158
258
|
# permit only allowed data mediatypes
|
|
159
|
-
|
|
160
|
-
mediatype, _ = mediatype.split(/[;,]/)[0..1] if mediatype
|
|
161
|
-
return false if mediatype && !SafeList::ALLOWED_URI_DATA_MEDIATYPES.include?(mediatype)
|
|
259
|
+
return false unless SafeList::ALLOWED_URI_DATA_MEDIATYPES.include?(data_uri_mediatype(uri_string))
|
|
162
260
|
end
|
|
163
261
|
end
|
|
164
262
|
true
|
|
165
263
|
end
|
|
166
264
|
|
|
265
|
+
def decode_numeric_character_references(string)
|
|
266
|
+
string.gsub(NUMERIC_CHARACTER_REFERENCE) do |reference|
|
|
267
|
+
digits = ::Regexp.last_match(1)
|
|
268
|
+
hexadecimal = digits.start_with?("x", "X")
|
|
269
|
+
digits = digits[1..-1] if hexadecimal
|
|
270
|
+
significant_digits = digits.sub(/\A0+/, "")
|
|
271
|
+
|
|
272
|
+
# The largest code point is U+10FFFF: 7 decimal or 6 hexadecimal significant digits.
|
|
273
|
+
# Anything longer is out of range; skip it without building a large integer from it.
|
|
274
|
+
next reference if significant_digits.length > (hexadecimal ? 6 : 7)
|
|
275
|
+
|
|
276
|
+
codepoint = significant_digits.to_i(hexadecimal ? 16 : 10)
|
|
277
|
+
begin
|
|
278
|
+
codepoint.chr(Encoding::UTF_8)
|
|
279
|
+
rescue RangeError
|
|
280
|
+
reference
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
167
285
|
def scrub_uri_attribute(attr_node)
|
|
168
286
|
if allowed_uri?(attr_node.value)
|
|
169
287
|
false
|
|
@@ -206,10 +324,10 @@ module Loofah
|
|
|
206
324
|
|
|
207
325
|
def cdata_escape(node)
|
|
208
326
|
escaped_text = escape_tags(node.text)
|
|
209
|
-
if
|
|
210
|
-
node.document.create_text_node(escaped_text)
|
|
211
|
-
else
|
|
327
|
+
if node.cdata?
|
|
212
328
|
node.document.create_cdata(escaped_text)
|
|
329
|
+
else
|
|
330
|
+
node.document.create_text_node(escaped_text)
|
|
213
331
|
end
|
|
214
332
|
end
|
|
215
333
|
|
|
@@ -238,6 +356,20 @@ module Loofah
|
|
|
238
356
|
string
|
|
239
357
|
end
|
|
240
358
|
end
|
|
359
|
+
|
|
360
|
+
private
|
|
361
|
+
|
|
362
|
+
# Returns the mediatype of a data: URI per RFC 2397, or nil when the
|
|
363
|
+
# required comma is absent. allowed_uri? entity-decodes, downcases, and
|
|
364
|
+
# strips control characters before calling this. An omitted or malformed
|
|
365
|
+
# mediatype resolves to "text/plain", matching the WHATWG data: URL processor.
|
|
366
|
+
def data_uri_mediatype(uri_string)
|
|
367
|
+
metadata, comma, _data = uri_string.delete_prefix("data:").partition(",")
|
|
368
|
+
return nil if comma.empty?
|
|
369
|
+
|
|
370
|
+
mediatype = metadata.delete_suffix(";base64").split(";", 2).first.to_s.strip
|
|
371
|
+
mediatype.match?(DATA_URI_MEDIATYPE) ? mediatype : "text/plain"
|
|
372
|
+
end
|
|
241
373
|
end
|
|
242
374
|
end
|
|
243
375
|
end
|
data/lib/loofah/scrubbers.rb
CHANGED
|
@@ -201,7 +201,11 @@ module Loofah
|
|
|
201
201
|
return CONTINUE if node.namespaces.empty?
|
|
202
202
|
end
|
|
203
203
|
when Nokogiri::XML::Node::TEXT_NODE, Nokogiri::XML::Node::CDATA_SECTION_NODE
|
|
204
|
-
|
|
204
|
+
if HTML5::Scrub.cdata_needs_escaping?(node)
|
|
205
|
+
node.before(HTML5::Scrub.cdata_escape(node))
|
|
206
|
+
else
|
|
207
|
+
return CONTINUE
|
|
208
|
+
end
|
|
205
209
|
end
|
|
206
210
|
node.remove
|
|
207
211
|
STOP
|
|
@@ -361,37 +365,34 @@ module Loofah
|
|
|
361
365
|
#
|
|
362
366
|
class DoubleBreakpoint < Scrubber
|
|
363
367
|
def initialize # rubocop:disable Lint/MissingSuper
|
|
364
|
-
|
|
368
|
+
# a top-down walk would empty a paragraph before reaching a paragraph nested in it
|
|
369
|
+
@direction = :bottom_up
|
|
365
370
|
end
|
|
366
371
|
|
|
367
372
|
def scrub(node)
|
|
368
373
|
return CONTINUE unless (node.type == Nokogiri::XML::Node::ELEMENT_NODE) && (node.name == "p")
|
|
374
|
+
return CONTINUE unless node.at_xpath("./br[following-sibling::br]")
|
|
369
375
|
|
|
370
|
-
|
|
376
|
+
new_paragraph = node.add_previous_sibling("<p>").first
|
|
371
377
|
|
|
372
|
-
|
|
373
|
-
|
|
378
|
+
node.children.each do |child|
|
|
379
|
+
remove_blank_text_nodes(child)
|
|
380
|
+
end
|
|
374
381
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
382
|
+
node.children.each do |child|
|
|
383
|
+
# already unlinked
|
|
384
|
+
next if child.parent.nil?
|
|
378
385
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
child.next_sibling.unlink
|
|
386
|
-
child.unlink
|
|
387
|
-
else
|
|
388
|
-
child.parent = new_paragraph
|
|
389
|
-
end
|
|
386
|
+
if child.name == "br" && child.next_sibling&.name == "br"
|
|
387
|
+
new_paragraph = node.add_previous_sibling("<p>").first
|
|
388
|
+
child.next_sibling.unlink
|
|
389
|
+
child.unlink
|
|
390
|
+
else
|
|
391
|
+
child.parent = new_paragraph
|
|
390
392
|
end
|
|
391
|
-
|
|
392
|
-
paragraph_node.unlink
|
|
393
393
|
end
|
|
394
394
|
|
|
395
|
+
node.unlink
|
|
395
396
|
CONTINUE
|
|
396
397
|
end
|
|
397
398
|
|
data/lib/loofah/version.rb
CHANGED
data/lib/loofah.rb
CHANGED
|
@@ -5,13 +5,7 @@ require "nokogiri"
|
|
|
5
5
|
module Loofah
|
|
6
6
|
class << self
|
|
7
7
|
def html5_support?
|
|
8
|
-
|
|
9
|
-
# subclassing fix from https://github.com/sparklemotion/nokogiri/pull/2534
|
|
10
|
-
return @html5_support if defined? @html5_support
|
|
11
|
-
|
|
12
|
-
@html5_support =
|
|
13
|
-
Gem::Version.new(Nokogiri::VERSION) > Gem::Version.new("1.14.0") &&
|
|
14
|
-
Nokogiri.uses_gumbo?
|
|
8
|
+
Nokogiri.uses_gumbo?
|
|
15
9
|
end
|
|
16
10
|
end
|
|
17
11
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: loofah
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.25.
|
|
4
|
+
version: 2.25.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Dalessio
|
|
@@ -30,14 +30,14 @@ dependencies:
|
|
|
30
30
|
requirements:
|
|
31
31
|
- - ">="
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: 1.
|
|
33
|
+
version: 1.19.5
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: 1.
|
|
40
|
+
version: 1.19.5
|
|
41
41
|
description: |
|
|
42
42
|
Loofah is a general library for manipulating and transforming HTML/XML documents and fragments,
|
|
43
43
|
built on top of Nokogiri.
|
|
@@ -89,14 +89,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
89
89
|
requirements:
|
|
90
90
|
- - ">="
|
|
91
91
|
- !ruby/object:Gem::Version
|
|
92
|
-
version: 2.
|
|
92
|
+
version: 3.2.0
|
|
93
93
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
94
94
|
requirements:
|
|
95
95
|
- - ">="
|
|
96
96
|
- !ruby/object:Gem::Version
|
|
97
97
|
version: '0'
|
|
98
98
|
requirements: []
|
|
99
|
-
rubygems_version: 4.0.
|
|
99
|
+
rubygems_version: 4.0.20
|
|
100
100
|
specification_version: 4
|
|
101
101
|
summary: Loofah is a general library for manipulating and transforming HTML/XML documents
|
|
102
102
|
and fragments, built on top of Nokogiri.
|