loofah 2.24.1 → 2.25.2
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 +25 -0
- data/SECURITY.md +1 -1
- data/lib/loofah/html5/safelist.rb +6 -2
- data/lib/loofah/html5/scrub.rb +102 -15
- data/lib/loofah/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a721138b6b152897d72edddfb80cdb0f35200c39a8f37a2024b503e6cfacc95
|
|
4
|
+
data.tar.gz: b342436a5a4e544b504d3e4234935ecd09dd87eddb55be893a1105cfd6a627f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e8ffd17d4b47d19bbff8070fcfe0d111530e7f146abc3501deaed1eb3198481f5ae16ee597e2a0d033eeea8bda27a038d1832c7237e24067a36e724e037e06a9
|
|
7
|
+
data.tar.gz: 71f741d1fbcf65fc137b25906c183cf767c091b203210e06d59dbb8e209c12274c77415b03406b021eb16dd149e2f9822f70124cec0947494f955d3fd1e458ee
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.25.2 / 2026-07-15
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
|
|
7
|
+
* 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
|
|
8
|
+
* 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
|
|
9
|
+
* 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
|
|
10
|
+
|
|
11
|
+
### Improved
|
|
12
|
+
|
|
13
|
+
* 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
|
|
14
|
+
* 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
|
|
15
|
+
* 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
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## 2.25.1 / 2026-03-17
|
|
19
|
+
|
|
20
|
+
* 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
|
|
21
|
+
|
|
22
|
+
## 2.25.0 / 2025-12-15
|
|
23
|
+
|
|
24
|
+
* Extract `Loofah::HTML5::Scrub.allowed_uri?` which operates on a string. Previously this logic was coupled to the parsed tree in `.scrub_uri_attribute`. #300 @flavorjones
|
|
25
|
+
* Tightened up how entities and control characters are handled when detecting allowed URIs. #301 @flavorjones
|
|
26
|
+
|
|
27
|
+
|
|
3
28
|
## 2.24.1 / 2025-05-12
|
|
4
29
|
|
|
5
30
|
### Ruby support
|
data/SECURITY.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The Loofah core contributors take security very seriously and investigate all reported vulnerabilities.
|
|
4
4
|
|
|
5
|
-
If you would like to report a vulnerablity or have a security concern regarding Loofah, please [report it via
|
|
5
|
+
If you would like to report a vulnerablity or have a security concern regarding Loofah, please [report it via Github](https://github.com/flavorjones/loofah/security).
|
|
6
6
|
|
|
7
7
|
Your report will be acknowledged within 24 hours, and you'll receive a more detailed response within 72 hours indicating next steps in handling your report.
|
|
8
8
|
|
|
@@ -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",
|
|
@@ -978,7 +983,7 @@ module Loofah
|
|
|
978
983
|
"stroke-opacity",
|
|
979
984
|
])
|
|
980
985
|
|
|
981
|
-
PROTOCOL_SEPARATOR = /:|(�*58)|(&#
|
|
986
|
+
PROTOCOL_SEPARATOR = /:|(�*58)|(�*3a)|(%|%)3A/i
|
|
982
987
|
|
|
983
988
|
ACCEPTABLE_PROTOCOLS = Set.new([
|
|
984
989
|
"afs",
|
|
@@ -987,7 +992,6 @@ module Loofah
|
|
|
987
992
|
"data",
|
|
988
993
|
"ed2k",
|
|
989
994
|
"fax",
|
|
990
|
-
"feed",
|
|
991
995
|
"ftp",
|
|
992
996
|
"gopher",
|
|
993
997
|
"http",
|
data/lib/loofah/html5/scrub.rb
CHANGED
|
@@ -15,6 +15,43 @@ module Loofah
|
|
|
15
15
|
CSS_PROPERTY_STRING_WITHOUT_EMBEDDED_QUOTES = /\A(["'])?[^"']+\1\z/
|
|
16
16
|
DATA_ATTRIBUTE_NAME = /\Adata-[\w-]+\z/
|
|
17
17
|
|
|
18
|
+
# Decimal (`:`) or hexadecimal (`:`) form, with or without the trailing semicolon that
|
|
19
|
+
# CGI.unescapeHTML requires but browsers do not.
|
|
20
|
+
NUMERIC_CHARACTER_REFERENCE = /&#(x[0-9a-f]+|[0-9]+);?/i
|
|
21
|
+
|
|
22
|
+
# A scheme (RFC 3986) followed by a protocol separator. The separator must recognize the same
|
|
23
|
+
# encoded-colon forms as PROTOCOL_SEPARATOR, otherwise a scheme split by an encoded colon (for
|
|
24
|
+
# example "javascript:alert(1)") would not be recognized as having a scheme and would skip
|
|
25
|
+
# protocol validation.
|
|
26
|
+
URI_PROTOCOL_REGEX = /\A[a-z][a-z0-9+\-.]*#{SafeList::PROTOCOL_SEPARATOR}/
|
|
27
|
+
|
|
28
|
+
# Matches a valid MIME type "essence" (type "/" subtype, no parameters), used to
|
|
29
|
+
# decide whether a data: URI mediatype is well-formed; a non-match is not a valid
|
|
30
|
+
# MIME type, which the data: URL processor treats as text/plain. Specs:
|
|
31
|
+
#
|
|
32
|
+
# https://mimesniff.spec.whatwg.org/#valid-mime-type
|
|
33
|
+
# https://mimesniff.spec.whatwg.org/#mime-type-essence
|
|
34
|
+
# https://mimesniff.spec.whatwg.org/#http-token-code-point
|
|
35
|
+
#
|
|
36
|
+
# The character class below is the HTTP token set (tchar) from RFC 9110 section
|
|
37
|
+
# 5.6.2, https://www.rfc-editor.org/rfc/rfc9110#name-tokens :
|
|
38
|
+
#
|
|
39
|
+
# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^"
|
|
40
|
+
# / "_" / "`" / "|" / "~" / DIGIT / ALPHA
|
|
41
|
+
#
|
|
42
|
+
# ALPHA is written a-z, not a-zA-Z, because allowed_uri? downcases the input first.
|
|
43
|
+
DATA_URI_MEDIATYPE = %r{
|
|
44
|
+
\A
|
|
45
|
+
[a-z0-9!\#$%&'*+\-.^_`|~]+ # type: 1*tchar
|
|
46
|
+
/ # "/" is not a tchar, so it is the sole delimiter
|
|
47
|
+
[a-z0-9!\#$%&'*+\-.^_`|~]+ # subtype: 1*tchar
|
|
48
|
+
\z
|
|
49
|
+
}x
|
|
50
|
+
|
|
51
|
+
# HTML5 named character references for whitespace that browsers strip from
|
|
52
|
+
# URIs. CGI.unescapeHTML does not decode these, so they are handled explicitly.
|
|
53
|
+
WHITESPACE_CHARACTER_REFERENCES = /&(Tab|NewLine);/
|
|
54
|
+
|
|
18
55
|
class << self
|
|
19
56
|
def allowed_element?(element_name)
|
|
20
57
|
::Loofah::HTML5::SafeList::ALLOWED_ELEMENTS_WITH_LIBXML2.include?(element_name)
|
|
@@ -47,7 +84,7 @@ module Loofah
|
|
|
47
84
|
end
|
|
48
85
|
|
|
49
86
|
next unless SafeList::SVG_ALLOW_LOCAL_HREF.include?(node.name) &&
|
|
50
|
-
attr_name
|
|
87
|
+
SafeList::SVG_HREF_ATTRIBUTES.include?(attr_name) &&
|
|
51
88
|
attr_node.value =~ /^\s*[^#\s].*/m
|
|
52
89
|
|
|
53
90
|
attr_node.remove
|
|
@@ -140,23 +177,59 @@ module Loofah
|
|
|
140
177
|
attr_node.value = values.join(" ")
|
|
141
178
|
end
|
|
142
179
|
|
|
180
|
+
# Returns true if the given URI string is safe, false otherwise. This method can be used to
|
|
181
|
+
# validate URI attribute values without requiring a Nokogiri DOM node.
|
|
182
|
+
def allowed_uri?(uri_string)
|
|
183
|
+
# CGI.unescapeHTML decodes numeric references only when they carry a trailing semicolon, so
|
|
184
|
+
# also decode the semicolon-less ones, which browsers still decode and execute. Normalizing
|
|
185
|
+
# more aggressively than a browser only rejects more, which is safe. Control characters are
|
|
186
|
+
# stripped both before and after decoding, since decoding can produce them. That strip must
|
|
187
|
+
# precede WHITESPACE_CHARACTER_REFERENCES: removing a control character can reveal a named
|
|
188
|
+
# whitespace reference.
|
|
189
|
+
uri_string = decode_numeric_character_references(CGI.unescapeHTML(uri_string.gsub(CONTROL_CHARACTERS, "")))
|
|
190
|
+
uri_string.gsub!(CONTROL_CHARACTERS, "")
|
|
191
|
+
uri_string.gsub!(WHITESPACE_CHARACTER_REFERENCES, "")
|
|
192
|
+
uri_string.gsub!(":", ":")
|
|
193
|
+
uri_string.downcase!
|
|
194
|
+
if URI_PROTOCOL_REGEX.match?(uri_string)
|
|
195
|
+
protocol = uri_string.split(SafeList::PROTOCOL_SEPARATOR)[0]
|
|
196
|
+
return false unless SafeList::ALLOWED_PROTOCOLS.include?(protocol)
|
|
197
|
+
|
|
198
|
+
if protocol == "data"
|
|
199
|
+
# permit only allowed data mediatypes
|
|
200
|
+
return false unless SafeList::ALLOWED_URI_DATA_MEDIATYPES.include?(data_uri_mediatype(uri_string))
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
true
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def decode_numeric_character_references(string)
|
|
207
|
+
string.gsub(NUMERIC_CHARACTER_REFERENCE) do |reference|
|
|
208
|
+
digits = ::Regexp.last_match(1)
|
|
209
|
+
hexadecimal = digits.start_with?("x", "X")
|
|
210
|
+
digits = digits[1..-1] if hexadecimal
|
|
211
|
+
significant_digits = digits.sub(/\A0+/, "")
|
|
212
|
+
|
|
213
|
+
# The largest code point is U+10FFFF: 7 decimal or 6 hexadecimal significant digits.
|
|
214
|
+
# Anything longer is out of range; skip it without building a large integer from it.
|
|
215
|
+
next reference if significant_digits.length > (hexadecimal ? 6 : 7)
|
|
216
|
+
|
|
217
|
+
codepoint = significant_digits.to_i(hexadecimal ? 16 : 10)
|
|
218
|
+
begin
|
|
219
|
+
codepoint.chr(Encoding::UTF_8)
|
|
220
|
+
rescue RangeError
|
|
221
|
+
reference
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
143
226
|
def scrub_uri_attribute(attr_node)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
!SafeList::ALLOWED_PROTOCOLS.include?(val_unescaped.split(SafeList::PROTOCOL_SEPARATOR)[0])
|
|
227
|
+
if allowed_uri?(attr_node.value)
|
|
228
|
+
false
|
|
229
|
+
else
|
|
148
230
|
attr_node.remove
|
|
149
|
-
|
|
150
|
-
elsif val_unescaped.split(SafeList::PROTOCOL_SEPARATOR)[0] == "data"
|
|
151
|
-
# permit only allowed data mediatypes
|
|
152
|
-
mediatype = val_unescaped.split(SafeList::PROTOCOL_SEPARATOR)[1]
|
|
153
|
-
mediatype, _ = mediatype.split(";")[0..1] if mediatype
|
|
154
|
-
if mediatype && !SafeList::ALLOWED_URI_DATA_MEDIATYPES.include?(mediatype)
|
|
155
|
-
attr_node.remove
|
|
156
|
-
return true
|
|
157
|
-
end
|
|
231
|
+
true
|
|
158
232
|
end
|
|
159
|
-
false
|
|
160
233
|
end
|
|
161
234
|
|
|
162
235
|
#
|
|
@@ -224,6 +297,20 @@ module Loofah
|
|
|
224
297
|
string
|
|
225
298
|
end
|
|
226
299
|
end
|
|
300
|
+
|
|
301
|
+
private
|
|
302
|
+
|
|
303
|
+
# Returns the mediatype of a data: URI per RFC 2397, or nil when the
|
|
304
|
+
# required comma is absent. allowed_uri? entity-decodes, downcases, and
|
|
305
|
+
# strips control characters before calling this. An omitted or malformed
|
|
306
|
+
# mediatype resolves to "text/plain", matching the WHATWG data: URL processor.
|
|
307
|
+
def data_uri_mediatype(uri_string)
|
|
308
|
+
metadata, comma, _data = uri_string.delete_prefix("data:").partition(",")
|
|
309
|
+
return nil if comma.empty?
|
|
310
|
+
|
|
311
|
+
mediatype = metadata.delete_suffix(";base64").split(";", 2).first.to_s.strip
|
|
312
|
+
mediatype.match?(DATA_URI_MEDIATYPE) ? mediatype : "text/plain"
|
|
313
|
+
end
|
|
227
314
|
end
|
|
228
315
|
end
|
|
229
316
|
end
|
data/lib/loofah/version.rb
CHANGED
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.
|
|
4
|
+
version: 2.25.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Dalessio
|
|
@@ -96,7 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
96
96
|
- !ruby/object:Gem::Version
|
|
97
97
|
version: '0'
|
|
98
98
|
requirements: []
|
|
99
|
-
rubygems_version:
|
|
99
|
+
rubygems_version: 4.0.10
|
|
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.
|