Skip to content

Commit 7052ee8

Browse files
committed
1.10.2
1 parent 61c0303 commit 7052ee8

File tree

8 files changed

+6913
-10
lines changed

8 files changed

+6913
-10
lines changed

.rubocop.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,6 @@ Naming/MethodName:
2525

2626
Style/ReturnNilInPredicateMethodDefinition:
2727
Enabled: false
28+
29+
Lint/UnreachableLoop:
30+
Enabled: false

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ group :test do
1111
gem "async"
1212
end
1313
gem "concurrent-ruby"
14+
gem "selenium-webdriver"
1415
end
1516

1617
group :development do

browser_tests.rb

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require "phlex"
5+
require "selenium-webdriver"
6+
7+
ASCII_CHARS = (0..127).to_set(&:chr)
8+
9+
class Layout < Phlex::HTML
10+
def view_template(&block)
11+
doctype
12+
html do
13+
head do
14+
meta(charset: "utf-8")
15+
end
16+
17+
body(&block)
18+
end
19+
end
20+
end
21+
22+
class JavaScriptLinks < Phlex::HTML
23+
def initialize(char)
24+
@char = char
25+
end
26+
27+
def view_template
28+
render Layout do
29+
# Standard JavaScript link
30+
a(href: "javascript:alert(1)") { "x" }
31+
32+
# With capitalization
33+
a(href: "Javascript:alert(1)") { "x" }
34+
35+
# With extra "javascript:" prefixes
36+
a(href: "javascript:javascript:alert(1)") { "x" }
37+
a(href: "javascript:javascript:javascript:alert(1)") { "x" }
38+
39+
# With extra "javascript:" prefixes and capitalization
40+
a(href: "javascript:Javascript:alert(1)") { "x" }
41+
a(href: "Javascript:javascript:alert(1)") { "x" }
42+
43+
a(href: "#{@char}javascript:alert(1)") { "x" }
44+
a(href: "j#{@char}avascript:alert(1)") { "x" }
45+
a(href: "ja#{@char}vascript:alert(1)") { "x" }
46+
a(href: "jav#{@char}ascript:alert(1)") { "x" }
47+
a(href: "java#{@char}script:alert(1)") { "x" }
48+
a(href: "javas#{@char}cript:alert(1)") { "x" }
49+
a(href: "javasc#{@char}ript:alert(1)") { "x" }
50+
a(href: "javascr#{@char}ipt:alert(1)") { "x" }
51+
a(href: "javascr#{@char}ipt:alert(1)") { "x" }
52+
a(href: "javascri#{@char}pt:alert(1)") { "x" }
53+
a(href: "javascrip#{@char}t:alert(1)") { "x" }
54+
a(href: "javascript#{@char}:alert(1)") { "x" }
55+
a(href: "javascript:#{@char}alert(1)") { "x" }
56+
57+
a(href: "#{@char}#{@char}javascript:alert(1)") { "x" }
58+
a(href: "j#{@char}#{@char}avascript:alert(1)") { "x" }
59+
a(href: "ja#{@char}#{@char}vascript:alert(1)") { "x" }
60+
a(href: "jav#{@char}#{@char}ascript:alert(1)") { "x" }
61+
a(href: "java#{@char}#{@char}script:alert(1)") { "x" }
62+
a(href: "javas#{@char}#{@char}cript:alert(1)") { "x" }
63+
a(href: "javasc#{@char}#{@char}ript:alert(1)") { "x" }
64+
a(href: "javascr#{@char}#{@char}ipt:alert(1)") { "x" }
65+
a(href: "javascr#{@char}#{@char}ipt:alert(1)") { "x" }
66+
a(href: "javascri#{@char}#{@char}pt:alert(1)") { "x" }
67+
a(href: "javascrip#{@char}#{@char}t:alert(1)") { "x" }
68+
a(href: "javascript#{@char}#{@char}:alert(1)") { "x" }
69+
a(href: "javascript:#{@char}#{@char}alert(1)") { "x" }
70+
71+
a(href: "#{@char}Javascript:alert(1)") { "x" }
72+
a(href: "J#{@char}avascript:alert(1)") { "x" }
73+
a(href: "Ja#{@char}vascript:alert(1)") { "x" }
74+
a(href: "Jav#{@char}ascript:alert(1)") { "x" }
75+
a(href: "Java#{@char}script:alert(1)") { "x" }
76+
a(href: "Javas#{@char}cript:alert(1)") { "x" }
77+
a(href: "Javasc#{@char}ript:alert(1)") { "x" }
78+
a(href: "Javascr#{@char}ipt:alert(1)") { "x" }
79+
a(href: "Javascr#{@char}ipt:alert(1)") { "x" }
80+
a(href: "Javascri#{@char}pt:alert(1)") { "x" }
81+
a(href: "Javascrip#{@char}t:alert(1)") { "x" }
82+
a(href: "Javascript#{@char}:alert(1)") { "x" }
83+
a(href: "Javascript:#{@char}alert(1)") { "x" }
84+
85+
a(href: "#{@char}#{@char}Javascript:alert(1)") { "x" }
86+
a(href: "J#{@char}#{@char}avascript:alert(1)") { "x" }
87+
a(href: "Ja#{@char}#{@char}vascript:alert(1)") { "x" }
88+
a(href: "Jav#{@char}#{@char}ascript:alert(1)") { "x" }
89+
a(href: "Java#{@char}#{@char}script:alert(1)") { "x" }
90+
a(href: "Javas#{@char}#{@char}cript:alert(1)") { "x" }
91+
a(href: "Javasc#{@char}#{@char}ript:alert(1)") { "x" }
92+
a(href: "Javascr#{@char}#{@char}ipt:alert(1)") { "x" }
93+
a(href: "Javascr#{@char}#{@char}ipt:alert(1)") { "x" }
94+
a(href: "Javascri#{@char}#{@char}pt:alert(1)") { "x" }
95+
a(href: "Javascrip#{@char}#{@char}t:alert(1)") { "x" }
96+
a(href: "Javascript#{@char}#{@char}:alert(1)") { "x" }
97+
a(href: "Javascript:#{@char}#{@char}alert(1)") { "x" }
98+
end
99+
end
100+
end
101+
102+
class XSSWithStrings < Phlex::HTML
103+
def view_template
104+
render Layout do
105+
File.open("fixtures/xss.txt") do |file|
106+
file.each_line do |line|
107+
div(class: line) { line }
108+
end
109+
end
110+
end
111+
end
112+
end
113+
114+
class XSSWithSymbols < Phlex::HTML
115+
def view_template
116+
render Layout do
117+
File.open("fixtures/xss.txt") do |file|
118+
file.each_line do |line|
119+
div(class: line.to_sym) { line.to_sym }
120+
end
121+
end
122+
end
123+
end
124+
end
125+
126+
class OnClick < Phlex::HTML
127+
def initialize(char)
128+
@char = char
129+
end
130+
131+
def view_template
132+
render Layout do
133+
ignore_warnings { div("#{@char}onclick" => "alert(1)") { "x" } }
134+
ignore_warnings { div("o#{@char}nclick" => "alert(1)") { "x" } }
135+
ignore_warnings { div("on#{@char}click" => "alert(1)") { "x" } }
136+
ignore_warnings { div("onc#{@char}lick" => "alert(1)") { "x" } }
137+
ignore_warnings { div("oncl#{@char}ick" => "alert(1)") { "x" } }
138+
ignore_warnings { div("oncli#{@char}ck" => "alert(1)") { "x" } }
139+
ignore_warnings { div("onclic#{@char}k" => "alert(1)") { "x" } }
140+
ignore_warnings { div("onclick#{@char}" => "alert(1)") { "x" } }
141+
142+
ignore_warnings { div("#{@char}#{@char}onclick" => "alert(1)") { "x" } }
143+
ignore_warnings { div("o#{@char}#{@char}nclick" => "alert(1)") { "x" } }
144+
ignore_warnings { div("on#{@char}#{@char}click" => "alert(1)") { "x" } }
145+
ignore_warnings { div("onc#{@char}#{@char}lick" => "alert(1)") { "x" } }
146+
ignore_warnings { div("oncl#{@char}#{@char}ick" => "alert(1)") { "x" } }
147+
ignore_warnings { div("oncli#{@char}#{@char}ck" => "alert(1)") { "x" } }
148+
ignore_warnings { div("onclic#{@char}#{@char}k" => "alert(1)") { "x" } }
149+
ignore_warnings { div("onclick#{@char}#{@char}" => "alert(1)") { "x" } }
150+
151+
ignore_warnings { div("#{@char}onclick": "alert(1)") { "x" } }
152+
ignore_warnings { div("o#{@char}nclick": "alert(1)") { "x" } }
153+
ignore_warnings { div("on#{@char}click": "alert(1)") { "x" } }
154+
ignore_warnings { div("onc#{@char}lick": "alert(1)") { "x" } }
155+
ignore_warnings { div("oncl#{@char}ick": "alert(1)") { "x" } }
156+
ignore_warnings { div("oncli#{@char}ck": "alert(1)") { "x" } }
157+
ignore_warnings { div("onclic#{@char}k": "alert(1)") { "x" } }
158+
ignore_warnings { div("onclick#{@char}": "alert(1)") { "x" } }
159+
160+
ignore_warnings { div("#{@char}#{@char}onclick": "alert(1)") { "x" } }
161+
ignore_warnings { div("o#{@char}#{@char}nclick": "alert(1)") { "x" } }
162+
ignore_warnings { div("on#{@char}#{@char}click": "alert(1)") { "x" } }
163+
ignore_warnings { div("onc#{@char}#{@char}lick": "alert(1)") { "x" } }
164+
ignore_warnings { div("oncl#{@char}#{@char}ick": "alert(1)") { "x" } }
165+
ignore_warnings { div("oncli#{@char}#{@char}ck": "alert(1)") { "x" } }
166+
ignore_warnings { div("onclic#{@char}#{@char}k": "alert(1)") { "x" } }
167+
ignore_warnings { div("onclick#{@char}#{@char}": "alert(1)") { "x" } }
168+
end
169+
end
170+
171+
def ignore_warnings
172+
yield
173+
rescue ArgumentError
174+
# ignore
175+
end
176+
end
177+
178+
class Browser
179+
MUTEX = { safari: Mutex.new, chrome: Mutex.new, firefox: Mutex.new }
180+
181+
def self.open(driver)
182+
MUTEX.fetch(driver).synchronize do
183+
browser = new(Selenium::WebDriver.for(driver))
184+
yield(browser)
185+
browser.quit
186+
end
187+
end
188+
189+
def self.open_each
190+
[:safari, :chrome, :firefox].map do |driver|
191+
Thread.new do
192+
self.open(driver) do |browser|
193+
yield(browser)
194+
end
195+
end
196+
end.each(&:join)
197+
end
198+
199+
def initialize(driver)
200+
@driver = driver
201+
end
202+
203+
attr_reader :driver
204+
205+
def load_string(string)
206+
navigate_to("data:text/html,#{ERB::Util.url_encode(string)}")
207+
end
208+
209+
def navigate_to(url)
210+
@driver.navigate.to(url)
211+
end
212+
213+
def execute_script(script)
214+
@driver.execute_script(script)
215+
end
216+
217+
def each_alert
218+
while (next_alert = alert)
219+
yield(next_alert)
220+
end
221+
end
222+
223+
def alert
224+
@driver.switch_to.alert
225+
rescue Selenium::WebDriver::Error::NoSuchAlertError
226+
nil
227+
end
228+
229+
def quit
230+
@driver.quit
231+
end
232+
end
233+
234+
Browser.open_each do |browser|
235+
ASCII_CHARS.each do |char|
236+
browser.load_string(JavaScriptLinks.new(char).call)
237+
browser.execute_script("document.querySelectorAll('a').forEach(function(a) { a.click(); });")
238+
browser.each_alert do |alert|
239+
unless alert.text == "Safari cannot open the page because the address is invalid."
240+
raise "Failed with #{char.codepoints}"
241+
end
242+
243+
alert.accept
244+
end
245+
246+
browser.load_string(OnClick.new(char).call)
247+
browser.execute_script("document.querySelectorAll('div').forEach(function(div) { div.click(); });")
248+
browser.each_alert do
249+
raise "Failed with #{char.codepoints}"
250+
end
251+
end
252+
253+
browser.load_string(XSSWithStrings.new.call)
254+
browser.execute_script("document.querySelectorAll('div').forEach(function(div) { div.click(); });")
255+
256+
if browser.alert
257+
raise "Failed with strings"
258+
end
259+
260+
browser.load_string(XSSWithSymbols.new.call)
261+
browser.execute_script("document.querySelectorAll('div').forEach(function(div) { div.click(); });")
262+
263+
if browser.alert
264+
raise "Failed with symbols"
265+
end
266+
end

0 commit comments

Comments
 (0)