Skip to content

Commit b04b18b

Browse files
committed
Add 30 new XSS maze categories with 180 test endpoints (825 total)
New categories: script gadget, encoding edge, multi-vector, sanitizer edge, hidden reflection, prototype, framework output, payload filter, late reflection, API response, attribute event, URL param context, SEO context, JS string escape, custom tag, embed context, global attr, error handling, ARIA attr, microdata, inline style, numeric context, boolean attr, list iteration, CMS pattern, ecommerce, dashboard, email template, social media, misc context. Fix conditional_reflect level2 to check digits instead of letter 'a'.
1 parent 45b92a5 commit b04b18b

32 files changed

+2257
-4
lines changed

src/mazes/api_response_xss.cr

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
def load_api_response_xss
2+
# Level 1: JSON-like response served as text/html
3+
# The query is reflected inside a JSON string value, but since Content-Type is text/html,
4+
# the browser will parse HTML tags embedded in the JSON.
5+
# Bypass: break out of JSON string with ", inject HTML like <img src=x onerror=alert(1)>
6+
Xssmaze.push("api-response-level1", "/api-response/level1/?query=a", "JSON-like response with text/html content type")
7+
maze_get "/api-response/level1/" do |env|
8+
query = env.params.query["query"]
9+
env.response.content_type = "text/html"
10+
11+
"{\"message\":\"#{query}\",\"status\":\"ok\"}"
12+
end
13+
14+
# Level 2: XML response served as text/html
15+
# The query is reflected inside an XML element, but Content-Type is text/html so
16+
# the browser renders HTML tags within the XML structure.
17+
# Bypass: inject <script>alert(1)</script> or <img src=x onerror=alert(1)>
18+
Xssmaze.push("api-response-level2", "/api-response/level2/?query=a", "XML response with text/html content type")
19+
maze_get "/api-response/level2/" do |env|
20+
query = env.params.query["query"]
21+
env.response.content_type = "text/html"
22+
23+
"<response><message>#{query}</message></response>"
24+
end
25+
26+
# Level 3: CSV-like response served as text/html
27+
# The query is reflected in a CSV-formatted body, but Content-Type is text/html,
28+
# so the browser treats the entire body as HTML.
29+
# Bypass: inject <script>alert(1)</script> directly
30+
Xssmaze.push("api-response-level3", "/api-response/level3/?query=a", "CSV-like response with text/html content type")
31+
maze_get "/api-response/level3/" do |env|
32+
query = env.params.query["query"]
33+
env.response.content_type = "text/html"
34+
35+
"name,value\nresult,#{query}"
36+
end
37+
38+
# Level 4: JSONP callback response served as text/html
39+
# The query is reflected inside JSONP data, but since Content-Type is text/html
40+
# the browser will parse any HTML injected into the data string.
41+
# Bypass: break out of JS string with ", inject </script><img src=x onerror=alert(1)>
42+
Xssmaze.push("api-response-level4", "/api-response/level4/?query=a", "JSONP callback response with text/html content type")
43+
maze_get "/api-response/level4/" do |env|
44+
query = env.params.query["query"]
45+
env.response.content_type = "text/html"
46+
47+
"jsonpCallback({\"data\":\"#{query}\"})"
48+
end
49+
50+
# Level 5: HTML fragment response (no doctype/html/body tags)
51+
# The query is reflected inside a bare HTML fragment with no wrapping document structure.
52+
# Bypass: inject <script>alert(1)</script> or event handlers to break out of div
53+
Xssmaze.push("api-response-level5", "/api-response/level5/?query=a", "HTML fragment response without full document structure")
54+
maze_get "/api-response/level5/" do |env|
55+
query = env.params.query["query"]
56+
env.response.content_type = "text/html"
57+
58+
"<div class=\"result\">#{query}</div>"
59+
end
60+
61+
# Level 6: Plain text error message served as text/html
62+
# A simple error string with the query reflected, but Content-Type is text/html
63+
# so the browser will render HTML tags embedded in the error text.
64+
# Bypass: inject <script>alert(1)</script> directly
65+
Xssmaze.push("api-response-level6", "/api-response/level6/?query=a", "Plain text error response with text/html content type")
66+
maze_get "/api-response/level6/" do |env|
67+
query = env.params.query["query"]
68+
env.response.content_type = "text/html"
69+
70+
"Error: #{query}"
71+
end
72+
end

src/mazes/aria_attr_xss.cr

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
def load_aria_attr_xss
2+
# Level 1: Reflected in <div role="alert" aria-label="QUERY">
3+
# Bypass: break out of aria-label with " then inject event handler
4+
Xssmaze.push("ariaattr-level1", "/ariaattr/level1/?query=a", "reflection in aria-label attribute (double-quoted)")
5+
maze_get "/ariaattr/level1/" do |env|
6+
query = env.params.query["query"]
7+
8+
"<html><body>
9+
<div role=\"alert\" aria-label=\"#{query}\">notification</div>
10+
</body></html>"
11+
end
12+
13+
# Level 2: Reflected in <input aria-describedby="QUERY" type="text">
14+
# Bypass: break out of aria-describedby with " then inject event handler
15+
Xssmaze.push("ariaattr-level2", "/ariaattr/level2/?query=a", "reflection in aria-describedby attribute (double-quoted)")
16+
maze_get "/ariaattr/level2/" do |env|
17+
query = env.params.query["query"]
18+
19+
"<html><body>
20+
<input aria-describedby=\"#{query}\" type=\"text\">
21+
</body></html>"
22+
end
23+
24+
# Level 3: Reflected in <span aria-live="polite" aria-atomic="QUERY">
25+
# Bypass: break out of aria-atomic with " then inject event handler
26+
Xssmaze.push("ariaattr-level3", "/ariaattr/level3/?query=a", "reflection in aria-atomic attribute (double-quoted)")
27+
maze_get "/ariaattr/level3/" do |env|
28+
query = env.params.query["query"]
29+
30+
"<html><body>
31+
<span aria-live=\"polite\" aria-atomic=\"#{query}\">updates</span>
32+
</body></html>"
33+
end
34+
35+
# Level 4: Reflected in <div role="tooltip" aria-hidden="QUERY">
36+
# Bypass: break out of aria-hidden with " then inject event handler
37+
Xssmaze.push("ariaattr-level4", "/ariaattr/level4/?query=a", "reflection in aria-hidden attribute (double-quoted)")
38+
maze_get "/ariaattr/level4/" do |env|
39+
query = env.params.query["query"]
40+
41+
"<html><body>
42+
<div role=\"tooltip\" aria-hidden=\"#{query}\">tooltip text</div>
43+
</body></html>"
44+
end
45+
46+
# Level 5: Reflected in <nav aria-label="QUERY">
47+
# Bypass: break out of aria-label with " then inject event handler
48+
Xssmaze.push("ariaattr-level5", "/ariaattr/level5/?query=a", "reflection in nav aria-label attribute (double-quoted)")
49+
maze_get "/ariaattr/level5/" do |env|
50+
query = env.params.query["query"]
51+
52+
"<html><body>
53+
<nav aria-label=\"#{query}\">
54+
<ul><li><a href=\"#\">Home</a></li></ul>
55+
</nav>
56+
</body></html>"
57+
end
58+
59+
# Level 6: Reflected in <div role="QUERY" aria-relevant="additions">
60+
# Bypass: break out of role with " then inject event handler
61+
Xssmaze.push("ariaattr-level6", "/ariaattr/level6/?query=a", "reflection in role attribute (double-quoted)")
62+
maze_get "/ariaattr/level6/" do |env|
63+
query = env.params.query["query"]
64+
65+
"<html><body>
66+
<div role=\"#{query}\" aria-relevant=\"additions\">content</div>
67+
</body></html>"
68+
end
69+
end

src/mazes/attribute_event_xss.cr

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
def load_attribute_event_xss
2+
# Level 1: Reflection inside onmouseover JS string
3+
# Query is placed inside a single-quoted JS string in an onmouseover event handler.
4+
# Bypass: break out with ') then inject new attribute/tag, e.g. ')//
5+
Xssmaze.push("attr-event-level1", "/attr-event/level1/?query=a", "reflection in onmouseover JS string context")
6+
maze_get "/attr-event/level1/" do |env|
7+
query = env.params.query["query"]
8+
9+
"<html><body><div onmouseover=\"show('#{query}')\">hover me</div></body></html>"
10+
end
11+
12+
# Level 2: Reflection inside onsubmit JS string
13+
# Query is placed inside a single-quoted JS string in an onsubmit handler.
14+
# Bypass: break out with ') then inject handler, e.g. ');alert(1)//
15+
Xssmaze.push("attr-event-level2", "/attr-event/level2/?query=a", "reflection in onsubmit JS string context")
16+
maze_get "/attr-event/level2/" do |env|
17+
query = env.params.query["query"]
18+
19+
"<html><body><form onsubmit=\"validate('#{query}'); return false\"><input type=\"submit\" value=\"Submit\"></form></body></html>"
20+
end
21+
22+
# Level 3: Reflection inside onload JS string
23+
# Query is placed inside a single-quoted JS string in a body onload handler.
24+
# Bypass: break out with ') then inject, e.g. ');alert(1)//
25+
Xssmaze.push("attr-event-level3", "/attr-event/level3/?query=a", "reflection in body onload JS string context")
26+
maze_get "/attr-event/level3/" do |env|
27+
query = env.params.query["query"]
28+
29+
"<html><body onload=\"init('#{query}')\"><p>Page loaded</p></body></html>"
30+
end
31+
32+
# Level 4: Reflection inside onerror JS string
33+
# Query is placed inside a single-quoted JS string in an img onerror handler.
34+
# The img src is intentionally invalid to trigger onerror.
35+
# Bypass: break out with ') then inject, e.g. ');alert(1)//
36+
Xssmaze.push("attr-event-level4", "/attr-event/level4/?query=a", "reflection in img onerror JS string context")
37+
maze_get "/attr-event/level4/" do |env|
38+
query = env.params.query["query"]
39+
40+
"<html><body><img src=\"/nonexistent.jpg\" onerror=\"report('#{query}')\"></body></html>"
41+
end
42+
43+
# Level 5: Reflection inside onblur JS string
44+
# Query is placed inside a single-quoted JS string in an input onblur handler.
45+
# Bypass: break out with ') then inject, e.g. ');alert(1)//
46+
Xssmaze.push("attr-event-level5", "/attr-event/level5/?query=a", "reflection in input onblur JS string context")
47+
maze_get "/attr-event/level5/" do |env|
48+
query = env.params.query["query"]
49+
50+
"<html><body><input value=\"test\" onblur=\"save('#{query}')\"></body></html>"
51+
end
52+
53+
# Level 6: Reflection inside onclick JS string
54+
# Query is placed inside a single-quoted JS string in a button onclick handler.
55+
# Bypass: break out with ') then inject, e.g. ');alert(1)//
56+
Xssmaze.push("attr-event-level6", "/attr-event/level6/?query=a", "reflection in button onclick JS string context")
57+
maze_get "/attr-event/level6/" do |env|
58+
query = env.params.query["query"]
59+
60+
"<html><body><button onclick=\"action('#{query}')\">Click</button></body></html>"
61+
end
62+
end

src/mazes/boolean_attr_xss.cr

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
def load_boolean_attr_xss
2+
# Level 1: Reflected in checkbox checked attribute value (double-quoted)
3+
Xssmaze.push("booleanattr-level1", "/booleanattr/level1/?query=true", "boolean attr context in checkbox checked")
4+
maze_get "/booleanattr/level1/" do |env|
5+
query = env.params.query["query"]
6+
7+
"<html><body>
8+
<h1>Boolean Attr XSS Level 1</h1>
9+
<form><input type=\"checkbox\" checked=\"#{query}\"> Accept terms</form>
10+
</body></html>"
11+
end
12+
13+
# Level 2: Reflected in select multiple attribute value (double-quoted)
14+
Xssmaze.push("booleanattr-level2", "/booleanattr/level2/?query=true", "boolean attr context in select multiple")
15+
maze_get "/booleanattr/level2/" do |env|
16+
query = env.params.query["query"]
17+
18+
"<html><body>
19+
<h1>Boolean Attr XSS Level 2</h1>
20+
<form><select multiple=\"#{query}\"><option>Option A</option><option>Option B</option></select></form>
21+
</body></html>"
22+
end
23+
24+
# Level 3: Reflected in input readonly attribute value (double-quoted)
25+
Xssmaze.push("booleanattr-level3", "/booleanattr/level3/?query=true", "boolean attr context in input readonly")
26+
maze_get "/booleanattr/level3/" do |env|
27+
query = env.params.query["query"]
28+
29+
"<html><body>
30+
<h1>Boolean Attr XSS Level 3</h1>
31+
<form><input type=\"text\" readonly=\"#{query}\" value=\"Read only field\"></form>
32+
</body></html>"
33+
end
34+
35+
# Level 4: Reflected in button disabled attribute value (double-quoted)
36+
Xssmaze.push("booleanattr-level4", "/booleanattr/level4/?query=true", "boolean attr context in button disabled")
37+
maze_get "/booleanattr/level4/" do |env|
38+
query = env.params.query["query"]
39+
40+
"<html><body>
41+
<h1>Boolean Attr XSS Level 4</h1>
42+
<form><button disabled=\"#{query}\">Submit</button></form>
43+
</body></html>"
44+
end
45+
46+
# Level 5: Reflected in details open attribute value (double-quoted)
47+
Xssmaze.push("booleanattr-level5", "/booleanattr/level5/?query=true", "boolean attr context in details open")
48+
maze_get "/booleanattr/level5/" do |env|
49+
query = env.params.query["query"]
50+
51+
"<html><body>
52+
<h1>Boolean Attr XSS Level 5</h1>
53+
<details open=\"#{query}\"><summary>Info</summary><p>Detailed content here.</p></details>
54+
</body></html>"
55+
end
56+
57+
# Level 6: Reflected in input autofocus attribute value (double-quoted)
58+
Xssmaze.push("booleanattr-level6", "/booleanattr/level6/?query=true", "boolean attr context in input autofocus")
59+
maze_get "/booleanattr/level6/" do |env|
60+
query = env.params.query["query"]
61+
62+
"<html><body>
63+
<h1>Boolean Attr XSS Level 6</h1>
64+
<form><input autofocus=\"#{query}\" type=\"text\" placeholder=\"Enter text\"></form>
65+
</body></html>"
66+
end
67+
end

src/mazes/cms_pattern_xss.cr

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
def load_cms_pattern_xss
2+
# Level 1: WordPress post title - raw reflection in entry-title h1
3+
# Bypass: direct HTML injection, e.g. <script>alert(1)</script>
4+
Xssmaze.push("cmspattern-level1", "/cmspattern/level1/?query=a", "WordPress post title raw reflection")
5+
maze_get "/cmspattern/level1/" do |env|
6+
query = env.params.query["query"]
7+
8+
"<html><body><h1 class=\"entry-title\">#{query}</h1></body></html>"
9+
end
10+
11+
# Level 2: WordPress widget title - raw reflection in widget-title h3
12+
# Bypass: direct HTML injection, e.g. <script>alert(1)</script>
13+
Xssmaze.push("cmspattern-level2", "/cmspattern/level2/?query=a", "WordPress widget title raw reflection")
14+
maze_get "/cmspattern/level2/" do |env|
15+
query = env.params.query["query"]
16+
17+
"<html><body><div class=\"widget\"><h3 class=\"widget-title\">#{query}</h3></div></body></html>"
18+
end
19+
20+
# Level 3: Drupal field body - raw reflection in field paragraph
21+
# Bypass: direct HTML injection, e.g. <script>alert(1)</script>
22+
Xssmaze.push("cmspattern-level3", "/cmspattern/level3/?query=a", "Drupal field body raw reflection")
23+
maze_get "/cmspattern/level3/" do |env|
24+
query = env.params.query["query"]
25+
26+
"<html><body><div class=\"field field--name-body\"><p>#{query}</p></div></body></html>"
27+
end
28+
29+
# Level 4: Joomla module - raw reflection in module heading
30+
# Bypass: direct HTML injection, e.g. <script>alert(1)</script>
31+
Xssmaze.push("cmspattern-level4", "/cmspattern/level4/?query=a", "Joomla module heading raw reflection")
32+
maze_get "/cmspattern/level4/" do |env|
33+
query = env.params.query["query"]
34+
35+
"<html><body><div class=\"moduletable\"><h3>#{query}</h3><div class=\"module-body\">content</div></div></body></html>"
36+
end
37+
38+
# Level 5: Ghost blog excerpt - raw reflection in post-card-excerpt
39+
# Bypass: direct HTML injection, e.g. <script>alert(1)</script>
40+
Xssmaze.push("cmspattern-level5", "/cmspattern/level5/?query=a", "Ghost blog excerpt raw reflection")
41+
maze_get "/cmspattern/level5/" do |env|
42+
query = env.params.query["query"]
43+
44+
"<html><body><p class=\"post-card-excerpt\">#{query}</p></body></html>"
45+
end
46+
47+
# Level 6: Medium-style article section - raw reflection in nested article content
48+
# Bypass: direct HTML injection, e.g. <script>alert(1)</script>
49+
Xssmaze.push("cmspattern-level6", "/cmspattern/level6/?query=a", "Medium-style article content raw reflection")
50+
maze_get "/cmspattern/level6/" do |env|
51+
query = env.params.query["query"]
52+
53+
"<html><body><article><section><div class=\"section-content\"><p>#{query}</p></div></section></article></body></html>"
54+
end
55+
end

src/mazes/conditional_reflect_xss.cr

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ def load_conditional_reflect_xss
1212
end
1313
end
1414

15-
# Level 2: Reflects only if input contains letter 'a'
16-
# Most payloads contain 'a' (alert, onerror, animate, etc.)
17-
Xssmaze.push("condreflect-level2", "/condreflect/level2/?query=a", "reflects only if input contains letter a")
15+
# Level 2: Reflects only if input contains a digit
16+
# Most payloads and scanner markers contain digits
17+
Xssmaze.push("condreflect-level2", "/condreflect/level2/?query=a", "reflects only if input contains a digit")
1818
maze_get "/condreflect/level2/" do |env|
1919
query = env.params.query["query"]
2020

21-
if query.includes?("a") || query.includes?("A")
21+
if query.matches?(/[0-9]/)
2222
"<html><body><div>#{query}</div></body></html>"
2323
else
2424
"<html><body><div>No results</div></body></html>"

0 commit comments

Comments
 (0)