Skip to content

Commit 556441d

Browse files
committed
Fix XSS issue
1 parent b75799e commit 556441d

File tree

8 files changed

+204
-7
lines changed

8 files changed

+204
-7
lines changed

Gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ gemspec
77

88
group :test do
99
gem "sus"
10-
gem "quickdraw", github: "joeldrapper/quickdraw"
10+
gem "quickdraw", git: "https://github.com/joeldrapper/quickdraw.git", ref: "06615ef2554dabec4fbf6cf2848fb9493842fd05"
1111
gem "simplecov", require: false
1212
gem "selenium-webdriver"
1313
end

browser_tests.rb

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,71 @@ def ignore_warnings
184184
end
185185
end
186186

187+
class EncodedJavaScriptLinks < Phlex::HTML
188+
def view_template
189+
render Layout do
190+
ignore_warnings { a(href: "javascript&#58;alert(1)") { "x" } }
191+
ignore_warnings { a(href: "java&#x73;cript:alert(1)") { "x" } }
192+
ignore_warnings { a(href: "javascript&#58alert(1)") { "x" } }
193+
ignore_warnings { a(href: "java&#x73cript:alert(1)") { "x" } }
194+
ignore_warnings { a(href: "javascript&colon;alert(1)") { "x" } }
195+
end
196+
end
197+
198+
def ignore_warnings
199+
yield
200+
rescue ArgumentError
201+
# ignore
202+
end
203+
end
204+
205+
class InjectedAttributeNames < Phlex::HTML
206+
def view_template
207+
render Layout do
208+
ignore_warnings { div("x onclick": "alert(1)") { "x" } }
209+
ignore_warnings { div("x/onclick": "alert(1)") { "x" } }
210+
end
211+
end
212+
213+
def ignore_warnings
214+
yield
215+
rescue ArgumentError
216+
# ignore
217+
end
218+
end
219+
220+
class UnsafeCustomTagNames < Phlex::HTML
221+
def view_template
222+
render Layout do
223+
ignore_warnings { tag(:"x-widget onclick=alert(1)") { "x" } }
224+
end
225+
end
226+
227+
def ignore_warnings
228+
yield
229+
rescue ArgumentError
230+
# ignore
231+
end
232+
end
233+
234+
class UnsafeSvgXlinkHref < Phlex::HTML
235+
def view_template
236+
render Layout do
237+
svg do |s|
238+
ignore_warnings { s.a("xlink:href": "javascript:alert(1)") { "x" } }
239+
ignore_warnings { s.a("xlink:href": "javascript&colon;alert(1)") { "x" } }
240+
ignore_warnings { s.a("xlink:href": "javascript&#58alert(1)") { "x" } }
241+
end
242+
end
243+
end
244+
245+
def ignore_warnings
246+
yield
247+
rescue ArgumentError
248+
# ignore
249+
end
250+
end
251+
187252
class Browser
188253
MUTEX = { safari: Mutex.new, chrome: Mutex.new, firefox: Mutex.new }
189254

@@ -272,4 +337,32 @@ def quit
272337
if browser.alert
273338
raise "Failed with symbols"
274339
end
340+
341+
browser.load_string(EncodedJavaScriptLinks.new.call)
342+
browser.execute_script("document.querySelectorAll('a').forEach(function(a) { a.click(); });")
343+
browser.each_alert do |alert|
344+
unless alert.text == "Safari cannot open the page because the address is invalid."
345+
raise "Failed with encoded javascript links"
346+
end
347+
348+
alert.accept
349+
end
350+
351+
browser.load_string(InjectedAttributeNames.new.call)
352+
browser.execute_script("document.querySelectorAll('div').forEach(function(div) { div.click(); });")
353+
browser.each_alert do
354+
raise "Failed with injected attribute names"
355+
end
356+
357+
browser.load_string(UnsafeCustomTagNames.new.call)
358+
browser.execute_script("document.querySelectorAll('x-widget').forEach(function(node) { node.click(); });")
359+
browser.each_alert do
360+
raise "Failed with unsafe custom tag names"
361+
end
362+
363+
browser.load_string(UnsafeSvgXlinkHref.new.call)
364+
browser.execute_script("document.querySelectorAll('svg a').forEach(function(node) { node.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); });")
365+
browser.each_alert do
366+
raise "Failed with unsafe SVG xlink href"
367+
end
275368
end

lib/phlex/html.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def tag(name, **attributes, &)
5555
raise Phlex::ArgumentError.new("Expected the tag name to be a Symbol.")
5656
end
5757

58-
if (tag = StandardElements.__registered_elements__[name]) || (tag = name.name.tr("_", "-")).include?("-")
58+
if (tag = StandardElements.__registered_elements__[name]) || ((tag = name.name.tr("_", "-")).include?("-") && tag.match?(/\A[a-z0-9-]+\z/))
5959
if attributes.length > 0 # with attributes
6060
if block_given # with content block
6161
buffer << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes)) << ">"

lib/phlex/sgml.rb

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
# **Standard Generalized Markup Language** for behaviour common to {HTML} and {SVG}.
44
class Phlex::SGML
55
UNSAFE_ATTRIBUTES = Set.new(%w[srcdoc sandbox http-equiv]).freeze
6-
REF_ATTRIBUTES = Set.new(%w[href src action formaction lowsrc dynsrc background ping]).freeze
6+
REF_ATTRIBUTES = Set.new(%w[href src action formaction lowsrc dynsrc background ping xlinkhref]).freeze
7+
NAMED_CHARACTER_REFERENCES = {
8+
"colon" => ":",
9+
"tab" => "\t",
10+
"newline" => "\n",
11+
}.freeze
12+
UNSAFE_ATTRIBUTE_NAME_CHARS = %r([<>&"'/=\s\x00])
713

814
ERBCompiler = ERB::Compiler.new("<>").tap do |compiler|
915
compiler.pre_cmd = [""]
@@ -518,7 +524,9 @@ def __attributes__(attributes, buffer = +"")
518524
if value != true && REF_ATTRIBUTES.include?(normalized_name)
519525
case value
520526
when String
521-
if value.downcase.delete("^a-z:").start_with?("javascript:")
527+
decoded_value = decode_html_character_references(value)
528+
529+
if decoded_value.downcase.delete("^a-z:").start_with?("javascript:")
522530
# We just ignore these because they were likely not specified by the developer.
523531
next
524532
end
@@ -536,7 +544,7 @@ def __attributes__(attributes, buffer = +"")
536544
end
537545
end
538546

539-
if name.match?(/[<>&"']/)
547+
if name.match?(UNSAFE_ATTRIBUTE_NAME_CHARS)
540548
raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
541549
end
542550

@@ -572,7 +580,7 @@ def __nested_attributes__(attributes, base_name, buffer = +"")
572580
else raise Phlex::ArgumentError.new("Attribute keys should be Strings or Symbols")
573581
end
574582

575-
if name.match?(/[<>&"']/)
583+
if name.match?(UNSAFE_ATTRIBUTE_NAME_CHARS)
576584
raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
577585
end
578586
end
@@ -653,6 +661,27 @@ def __nested_tokens__(tokens)
653661
buffer.gsub('"', "&quot;")
654662
end
655663

664+
private def decode_html_character_references(value)
665+
value
666+
.gsub(/&#x([0-9a-f]+);?/i) {
667+
begin
668+
[$1.to_i(16)].pack("U*")
669+
rescue
670+
""
671+
end
672+
}
673+
.gsub(/&#(\d+);?/) {
674+
begin
675+
[$1.to_i].pack("U*")
676+
rescue
677+
""
678+
end
679+
}
680+
.gsub(/&([a-z][a-z0-9]+);?/i) {
681+
NAMED_CHARACTER_REFERENCES[$1.downcase] || ""
682+
}
683+
end
684+
656685
# Result is **unsafe**, so it should be escaped!
657686
def __styles__(styles)
658687
case styles

lib/phlex/svg.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def tag(name, **attributes, &)
4141
raise Phlex::ArgumentError.new("Expected the tag name to be a Symbol.")
4242
end
4343

44-
if (tag = StandardElements.__registered_elements__[name]) || (tag = name.name.tr("_", "-")).include?("-")
44+
if (tag = StandardElements.__registered_elements__[name]) || ((tag = name.name.tr("_", "-")).include?("-") && tag.match?(/\A[a-z0-9-]+\z/))
4545
if attributes.length > 0 # with attributes
4646
if block_given # with content block
4747
buffer << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes)) << ">"

mise.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
[settings]
2+
idiomatic_version_file_enable_tools = ["ruby"]
3+
14
[tasks.setup]
25
description = "Install all project dependencies."
36
run = ["mise install", "bundle install --jobs 4"]

quickdraw/sgml/attributes.test.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,27 @@
4545
phlex { a("Href" => "javascript:alert('hello')") },
4646
phlex { a("Href" => "javascript:javascript:alert('hello')") },
4747
phlex { a(href: " \t\njavascript:alert('hello')") },
48+
phlex { a(href: "java&#x73;cript:alert(1)") },
49+
phlex { a(href: "javascript&#58;alert(1)") },
50+
phlex { a(href: "java&#115;cript:alert(1)") },
51+
phlex { a(href: "&#106;avascript:alert(1)") },
52+
phlex { a(href: "javascript&#58alert(1)") },
53+
phlex { a(href: "javascript&colon;alert(1)") },
4854
].each do |output|
4955
assert_equal_html output, %(<a></a>)
5056
end
5157
end
5258

59+
test "unsafe xlink:href attribute" do
60+
[
61+
phlex(Phlex::SVG) { a("xlink:href": "javascript:alert(1)") { "x" } },
62+
phlex(Phlex::SVG) { a("xlink:href": "javascript&colon;alert(1)") { "x" } },
63+
phlex(Phlex::SVG) { a("xlink:href": "javascript&#58alert(1)") { "x" } },
64+
].each do |output|
65+
assert_equal_html output, %(<a>x</a>)
66+
end
67+
end
68+
5369
test "unsafe attribute name <" do
5470
error = assert_raises(Phlex::ArgumentError) do
5571
phlex { div("<" => true) }
@@ -90,6 +106,38 @@
90106
assert_equal error.message, "Unsafe attribute name detected: \"."
91107
end
92108

109+
test "unsafe attribute name with space (String)" do
110+
error = assert_raises(Phlex::ArgumentError) do
111+
phlex { div("foo bar" => true) }
112+
end
113+
114+
assert_equal error.message, "Unsafe attribute name detected: foo bar."
115+
end
116+
117+
test "unsafe attribute name with space (Symbol)" do
118+
error = assert_raises(Phlex::ArgumentError) do
119+
phlex { div("foo bar": true) }
120+
end
121+
122+
assert_equal error.message, "Unsafe attribute name detected: foo bar."
123+
end
124+
125+
test "unsafe attribute name with slash (String)" do
126+
error = assert_raises(Phlex::ArgumentError) do
127+
phlex { div("foo/bar" => true) }
128+
end
129+
130+
assert_equal error.message, "Unsafe attribute name detected: foo/bar."
131+
end
132+
133+
test "unsafe attribute name with slash (Symbol)" do
134+
error = assert_raises(Phlex::ArgumentError) do
135+
phlex { div("foo/bar": true) }
136+
end
137+
138+
assert_equal error.message, "Unsafe attribute name detected: foo/bar."
139+
end
140+
93141
test "_, nil" do
94142
output = phlex { div(attribute: nil) }
95143
assert_equal_html output, %(<div></div>)

quickdraw/tag.test.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,27 @@ def view_template(&)
159159
<custom-tag></custom-tag>
160160
HTML
161161
end
162+
163+
test "with unsafe custom tag name containing a space" do
164+
error = assert_raises Phlex::ArgumentError do
165+
HTMLComponent.call(:"x-widget onclick=alert(1)")
166+
end
167+
168+
assert_equal error.message, "Invalid HTML tag: x-widget onclick=alert(1)"
169+
end
170+
171+
test "with unsafe custom tag name containing special characters" do
172+
error = assert_raises Phlex::ArgumentError do
173+
HTMLComponent.call(:"x-widget>")
174+
end
175+
176+
assert_equal error.message, "Invalid HTML tag: x-widget>"
177+
end
178+
179+
test "with unsafe SVG custom tag name containing a space" do
180+
error = assert_raises Phlex::ArgumentError do
181+
SVGComponent.call(:"x-widget onclick=alert(1)")
182+
end
183+
184+
assert_equal error.message, "Invalid SVG tag: x-widget onclick=alert(1)"
185+
end

0 commit comments

Comments
 (0)