Cross-site Scripting


Web Security.

Browsers render HTML, run JavaScript, and parse CSS. When a server drops user input into a page without escaping it, the browser can read that input as markup or script instead of text. Data becomes code.

This is Cross-Site Scripting (XSS). It comes in three flavors:

  • DOM-based (Type 0): the bug lives entirely in client-side JavaScript.
  • Reflected (Type 1): input is echoed straight back in the response.
  • Stored (Type 2): input is saved and served to other users later.

This module walks through how each type shows up and how small input-handling mistakes lead to full client-side compromise.



Reflected XSS

Cross-Site Scripting (XSS) is one of the most common web security vulnerabilities. It happens when a web application takes user input and displays it in the browser without handling or escaping it appropriately. When that input is treated as code instead of plain text, attackers can execute JavaScript in someone else's browser.

In this level, you'll explore how reflected input can turn into executable code. Your goal is simple: trigger a JavaScript alert().


Challenge Environment

The challenge files are located in /challenge.

To begin, start the web server: /challenge/server

Once running, you can access the website at: https://challenge.internal

You can visit it using a browser inside the Desktop workspace.

Once you've created a URL that triggers an alert(), go ahead and run: /challenge/victim.

When you're prompted, paste your crafted URL there. If your payload pops an alert in the victim's browser, you'll get the flag!


In this challenge, the server and the victim are isolated inside an air-gapped™ network namespace. This means the victim cannot access any external URLs or services outside that namespace, and its only reachable destination is the server itself.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Cross-Site Scripting is about more than just making the browser behave unexpectedly; it is also about what that behavior enables. When user input is reflected on a page without being properly handled, the injected JavaScript executes with the same privileges as the victim who sees it.

In practice, that means an attacker's code can access sensitive information stored in the browser, including session cookies that keep users authenticated.

In this level, you'll explore how a simple reflected XSS vulnerability can be used to retrieve a victim's cookie.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Cross-Site Scripting does not always show up in obvious places. Sometimes user input is reflected inside an HTML attribute, where one misplaced quote is enough to turn harmless text into executable behavior.

In this level, your input is reflected into an attribute value. Escape that context and use your payload to retrieve the victim's cookie.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Cross-Site Scripting gets trickier when the app stops handing you neat quote boundaries. If input lands in an unquoted attribute, spaces and delimiters become part of the game.

In this level, your input is reflected into an unquoted attribute value. Break out cleanly and use your payload to retrieve the victim's cookie.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Not every client-side injection bug starts in page text. A link target can be just as dangerous when it is built from untrusted input.

In this challenge, user input is placed directly into an href attribute. Your goal is to creatively extract the victim's authenticated token.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Message links now begin with an internal https://challenge.internal/preview?msg= prefix, and the obvious payload characters <> and / get stripped on the way in.

The victim will still follow the link, and the authenticated token is still there to be taken.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

The message box still trusts your input, but the page no longer wraps it in JavaScript. Your message goes straight into the HTML this time, as the content of a regular tag.

There is no script block to escape from, so the string-breakout trick from the previous levels does not apply. But a Content Security Policy is in place. The policy does not stop scripts from running, it just restricts where they may be loaded from.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Your message now passes through an inline JavaScript snippet before it ever reaches the page. The page assigns your input to a variable as a quoted string inside an existing <script> block.

Because your input is inside a string literal, the HTML never sees your message as markup. The JavaScript parser does.

A Content Security Policy blocks adding any new script tags, so the existing script is your only place to run code. Break out of the string literal and what comes after it is parsed as code instead of text.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Your message still passes through an inline JavaScript snippet before it reaches the page, but this time it lands inside a template literal instead of a regular quoted string.

Template literals use backticks, and they have their own syntax for inserting expressions. The rules for what counts as code inside them are not the same as for "..." strings.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Your message is now tucked into a JavaScript object before the page reads it back out.

Instead of being assigned to a string variable directly, your input sits as the value of a property inside an object literal like { key: "your input here" }. The surrounding code looks different from the previous level, but the JavaScript parser still has to figure out where your value ends and the rest of the script begins.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

The page tries to look safer by packing your message into JSON and parsing it in the browser.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

The preview now lives behind an iframe src, not an inline document. Your input still reaches a same-origin page, just one step later.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

The ability to read a server's log is powerful.

When the victim runs, they pick up a cookie containing the flag, and then visit whatever URL you handed them on challenge.internal. The cookie is readable from JavaScript on that origin.

You cannot exfiltrate it to an external server because the victim is air-gapped. But the server logs every request that comes in, and you can read its access log from your shell. Anything the victim's browser sends to the server ends up in the log.


Challenge Environment

In this challenge, the server and the victim are isolated inside an air-gapped™ network namespace. This means the victim cannot access any external URLs or services outside that namespace, and its only reachable destination is the server itself.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

In real systems, the bug is not always kind enough to hand you the secret outright. Sometimes all that is left is a tiny clue: a response that is a little slower. Even one single bit of information at a time can be enough.

That is the idea behind a side channel: leaking data through some secondary signal instead of the intended output. Timing is one of the most common examples.

Creatively leak the flag.


Challenge Environment

In this challenge, the server and the victim are isolated inside an air-gapped™ network namespace. This means the victim cannot access any external URLs or services outside that namespace, and its only reachable destination is the server itself.

To debug in practice mode, run the server with sudo and the logs come back.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Stored XSS

This new pwnpost™ platform looks unsafe...

Maybe an admin should check the posts out to see if they are safe?


Challenge Environment

You can login into pwnpost™ using these accounts:

  • guest:password
  • hacker:1337

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

pwnpost™ no longer supports admin posting due to rampant flag disclosure.

The flag is still saved as one of admin's drafts. Drafts are private to their author, so the flag never appears in the public feed and nobody else can pull it out of the database.

But admin still opens the feed to read everyone's posts. When they do, your post runs as JavaScript in their tab while they are logged in, with full access to admin's own draft from inside their browser.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

pwnpost™ now supports creating new accounts. But admin can't see the posts due to security concerns.

The admin still opens the feed to review activity. Every post shows the author's username next to it, and that username field is rendered without the same handling as the post body.

Register the right account, post anything as it, and the username does the work for you when admin loads the feed.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

What happens in pwnpost™, stays in pwnpost™.

Non-admin posts are now hidden from admin's feed view. When admin opens the feed, the body of your post is replaced with a placeholder, so a script in the content never runs in their tab.

There is also a content filter on drafts that rejects anything containing the obvious flag prefix.

But the page admin loads still has plenty going on. The post body is not the only place a server can render attacker-controlled text.


Challenge Environment

In this challenge, the server and the victim are isolated inside an air-gapped™ network namespace. This means the victim cannot access any external URLs or services outside that namespace, and its only reachable destination is the server itself.

To debug in practice mode, run the server with sudo and the logs come back.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

What happens in pwnpost™, stays in pwnpost™ for real this time.

Same visibility rules as before: non-admin post bodies are hidden when admin views the feed, replaced with a placeholder. Your content does not reach them through there.

This time there is no content filter on drafts, so you have cleaner room to operate on whatever does still get rendered to admin on the feed page.


Challenge Environment

In this challenge, the server and the victim are isolated inside an air-gapped™ network namespace. This means the victim cannot access any external URLs or services outside that namespace, and its only reachable destination is the server itself.

To debug in practice mode, run the server with sudo and the logs come back.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

How to stop hackers from leaking admin's private posts? By not posting at all!!!

Modern problems require modern solutions.

The admin now opens the feed, starts typing a draft that contains the flag, and never submits it. The text only exists in their browser, inside the textarea on the page.

You still get one stored payload running in their tab when they view the feed. The keys are typed in front of you, before the page ever sees a submitted post.


Challenge Environment

In this challenge, the server and the victim are isolated inside an air-gapped™ network namespace. This means the victim cannot access any external URLs or services outside that namespace, and its only reachable destination is the server itself.

To debug in practice mode, run the server with sudo and the logs come back.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Injection into HTML attributes doesn't always require JavaScript. When a Content Security Policy blocks script execution entirely, an attacker who controls part of an attribute value may still be able to exfiltrate data from the page using nothing but HTML. No code required.

In this challenge, your input is reflected into an HTML attribute without sanitization. A Content Security Policy prevents JavaScript execution.

The admin's token is only on the page when admin is viewing it.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

pwnpost™ now supports user avatars. Files can be uploaded at /profile and are served back at /avatar/<username>.

File upload validation is a surprisingly difficult problem. There are many ways to determine what kind of file something is, and they don't always agree with each other.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Stored XSS doesn't always fire in the attacker's own browser. When the vulnerable page is restricted to privileged users, your payload executes somewhere you can't see. The results have to arrive out-of-band. This is called blind XSS, and it's how real-world XSS canaries work.

pwnpost™ now has a bug reporting system. The admin reviews submitted reports.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Same setup as the previous challenge: blind XSS via the bug reporting system, admin reviews at /reports.

This time, the session cookie is HttpOnly. But the flag is still somewhere the admin's browser can reach.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

DOM XSS

The server is no longer reflecting your input into the page. That does not mean the page stopped trusting it.

In this level, the dangerous part happens entirely in the browser. Client-side JavaScript reads data from the URL and writes it back into the DOM. This is DOM-based XSS: the bug lives in the page's own logic, not in the server response.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

The browser is still doing the dangerous part. This time it reads from the query string instead of the hash.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Data parked in one DOM attribute can turn into HTML again if later code treats it like markup.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Not every DOM bug goes straight from the URL into the page. Sometimes the browser stores attacker input in harmless-looking metadata first, then another script turns it back into markup later.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

The ephemeral page does not trust the URL anymore. It trusts whoever can message it. Without an origin check, another window can make this preview treat attacker data like local content.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Mutation XSS

There are not a lot of articles about mXSS, but here are some good ones to read.

A sanitizer that removes dangerous tags and strips event handlers should make HTML safe. But "safe" depends on who is doing the parsing. When a server sanitizes HTML with one parser and then the browser re-parses the sanitized output via innerHTML, the two parsers may not agree on what the HTML means. The difference can be exploitable.

This is called Mutation XSS (mXSS): the payload mutates between sanitization and rendering.

In this challenge, the server uses Python's html5lib (via BeautifulSoup) to sanitize your input. The sanitized HTML is then rendered via innerHTML in the browser.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Parsers track more than just element nesting. They also track context and namespace. How an element is serialized, and how it's re-parsed, can differ depending on where in the document tree it appears.

In MathML, <annotation-xml> is special: its encoding attribute declares what namespace its content lives in. The same element can mean something entirely different depending on where it appears and how it declares its content. Sanitizers and browsers don't always agree on what that means.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Filters

Through this series of challenges, you will become familiar with the concept of XSS filters bypass!

In this challenge, the following element is banned:

  • script (case sensitive)

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Through this series of challenges, you will become familiar with the concept of XSS filters bypass!

In this challenge, the following elements are banned:

  • script, img

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Through this series of challenges, you will become familiar with the concept of XSS filters bypass!

In this challenge, the following elements and patterns are banned:

  • script
  • img
  • on*= event handler attributes (e.g. onclick=, onerror=, onload=)

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Through this series of challenges, you will become familiar with the concept of XSS filters bypass!

In this challenge, the following elements are banned:

  • script, img, svg, iframe, input
  • video, audio, source, track

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Through this series of challenges, you will become familiar with the concept of XSS filters bypass!

In this challenge, the following elements are banned:

  • script, img, svg, iframe, input
  • video, audio, source, track
  • html, body, marquee, details

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Through this series of challenges, you will become familiar with the concept of XSS filters bypass!

In this challenge, the following elements are banned:

  • script, img, svg, iframe, input
  • video, audio, source, track
  • html, body, frameset, details, dialog

The following handler is also banned:

  • ontoggle

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Through this series of challenges, you will become familiar with the concept of XSS filters bypass!

In this challenge, the following elements are banned:

  • script, img, svg, iframe, input
  • video, audio, source, track
  • html, body, frameset, details, dialog, marquee

The following handlers are also banned:

  • ontoggle, onstart
  • onload, onerror, onclick
  • onfocus, onfocusin, onfocusout, autofocus

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Through this series of challenges, you will become familiar with the concept of XSS filters bypass!

In this challenge, the following elements are banned:

  • script, img, svg, iframe, input
  • video, audio, source, track
  • html, body, frameset, details, dialog, marquee

The following handlers are also banned:

  • ontoggle, onstart
  • onload, onerror, onclick
  • onfocus, onfocusin, onfocusout, autofocus

The following attribute is also banned:

  • style=

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Through this series of challenges, you will become familiar with the concept of XSS filters bypass!

In this challenge, the following elements are banned:

  • script, img, svg, iframe, input
  • video, audio, source, track
  • html, body, frameset, details, dialog, marquee

The following handlers are also banned:

  • ontoggle, onstart
  • onload, onerror, onclick
  • onfocus, onfocusin, onfocusout, autofocus
  • onanimationstart, onanimationiteration, onanimationend, onanimationcancel

The following attributes are also banned:

  • style=, animation, keyframes

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Through this series of challenges, you will become familiar with the concept of XSS filters bypass!

In this challenge, the following elements are banned:

  • script, img, svg, iframe, input
  • video, audio, source, track
  • html, body, frameset, details, dialog, marquee
  • style, link

The following handlers are also banned:

  • ontoggle, onstart
  • onload, onerror, onclick
  • onfocus, onfocusin, onfocusout, autofocus
  • onanimationstart, onanimationiteration, onanimationend, onanimationcancel

The following attributes are also banned:

  • style=, animation, keyframes
  • transition, transform

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Through this series of challenges, you will become familiar with the concept of XSS filters bypass!

In this challenge, the following elements are banned:

  • script, img, svg, iframe, input
  • video, audio, source, track
  • html, body, frameset, details, dialog, marquee
  • style, link, template

The following handlers are also banned:

  • ontoggle, onstart
  • onload, onerror, onclick
  • onfocus, onfocusin, onfocusout, autofocus
  • onanimationstart, onanimationiteration, onanimationend, onanimationcancel

The following attributes are also banned:

  • style=, animation, keyframes, content-visibility
  • transition, transform

The following shadow DOM primitives are also banned:

  • shadow, slot

A Content Security Policy is also applied: style-src 'nonce-{random}'; style-src-attr 'none'

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Through this series of challenges, you will become familiar with the concept of XSS filters bypass!

In this challenge, the following element is allowed:

  • script

The following APIs are banned:

  • fetch, XMLHttpRequest, sendBeacon

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Through this series of challenges, you will become familiar with the concept of XSS filters bypass!

In this challenge, the following element is allowed:

  • script

The following APIs are banned:

  • fetch, XMLHttpRequest, sendBeacon
  • location, open(), assign(), replace()

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Through this series of challenges, you will become familiar with the concept of XSS filters bypass!

In this challenge, the following element is allowed:

  • script

The following APIs are banned:

  • fetch, XMLHttpRequest, sendBeacon
  • location, open(), assign(), replace(), pushState, replaceState
  • submit, click, write, writeln
  • createElement, createElementNS, adoptNode, importNode, cloneNode
  • append, appendChild, prepend, before, after, insertBefore
  • insertAdjacentHTML, insertAdjacentText, insertAdjacentElement
  • replaceChildren, replaceChild, replaceWith, remove, removeChild
  • innerHTML, outerHTML, innerText, outerText, textContent
  • setAttribute, setAttributeNS, removeAttribute, removeAttributeNS, toggleAttribute
  • querySelector, querySelectorAll, getElementById, getElementsByTagName, getElementsByClassName, getElementsByName
  • href, action, formAction, srcdoc
  • cookieStore, localStorage, sessionStorage
  • FormData, URL, URLSearchParams, Request, Headers, Response
  • eval, Function, setTimeout, setInterval

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Through this series of challenges, you will become familiar with the concept of XSS filters bypass!

In this challenge, the following element is allowed:

  • script

The following APIs are banned:

  • fetch, XMLHttpRequest, sendBeacon
  • location, open(), assign(), replace(), pushState, replaceState
  • submit, requestSubmit, click, write, writeln
  • createElement, createElementNS, adoptNode, importNode, cloneNode
  • append, appendChild, prepend, before, after, insertBefore
  • insertAdjacentHTML, insertAdjacentText, insertAdjacentElement
  • replaceChildren, replaceChild, replaceWith, remove, removeChild
  • innerHTML, outerHTML, innerText, outerText, textContent
  • setAttribute, setAttributeNS, removeAttribute, removeAttributeNS, toggleAttribute
  • querySelector, querySelectorAll, getElementById, getElementsByTagName, getElementsByClassName, getElementsByName
  • href, src, action, formAction, srcdoc
  • cookie, cookieStore, localStorage, sessionStorage
  • FormData, URL, URLSearchParams, Request, Headers, Response
  • media, Image, Audio, Video, Track, Source, Bitmap, Canvas, Blob, File
  • navigator, postMessage, document, window, globalThis, self, top, frames, form, element
  • eval, Function, setTimeout, setInterval

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Through this series of challenges, you will become familiar with the concept of XSS filters bypass!

In this challenge, the following element is allowed:

  • script

The following APIs and globals are banned:

  • fetch, XMLHttpRequest, sendBeacon
  • location, open(), assign(), replace(), pushState, replaceState
  • submit, requestSubmit, click, write, writeln
  • createElement, createElementNS, adoptNode, importNode, cloneNode
  • append, appendChild, prepend, before, after, insertBefore
  • insertAdjacentHTML, insertAdjacentText, insertAdjacentElement
  • replaceChildren, replaceChild, replaceWith, remove, removeChild
  • innerHTML, outerHTML, innerText, outerText, textContent
  • setAttribute, setAttributeNS, removeAttribute, removeAttributeNS, toggleAttribute
  • querySelector, querySelectorAll, getElementById, getElementsByTagName, getElementsByClassName, getElementsByName
  • href, src, action, formAction, srcdoc
  • cookie, cookieStore, localStorage, sessionStorage
  • FormData, URL, URLSearchParams, Request, Headers, Response
  • media, Image, Audio, Video, Track, Source, Bitmap, Canvas, Blob, File
  • navigator, postMessage, document, window, globalThis, global, self, this, top, parent, frames, form, element, constructor
  • eval, Function, setTimeout, setInterval, import()

Computed member access via square brackets ([, ]) is also disallowed.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Through this series of challenges, you will become familiar with the concept of XSS filters bypass!

In this challenge, the following element is allowed:

  • script

Within that script block, only the following characters are allowed:

  • [, ], (, ), !, +

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Through this series of challenges, you will become familiar with the concept of XSS filters bypass!

In this challenge, all HTML elements are banned.

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

Through this series of challenges, you will become familiar with the concept of XSS filters bypass!

In this challenge, the following elements are banned:

  • script, img, svg, iframe, input
  • video, audio, source, track
  • html, body, frameset, details, dialog, marquee
  • style, link, template

The following handlers are also banned:

  • ontoggle, onstart
  • onload, onerror, onclick
  • onfocus, onfocusin, onfocusout, autofocus
  • onanimationstart, onanimationiteration, onanimationend, onanimationcancel

The following attributes are also banned:

  • style=, animation, keyframes, content-visibility
  • transition, transform

The following shadow DOM primitives are also banned:

  • shadow, slot

The following APIs and globals are also banned:

  • fetch, XMLHttpRequest, sendBeacon
  • location, open(), assign(), replace(), pushState, replaceState
  • submit, requestSubmit, click, write, writeln
  • createElement, createElementNS, adoptNode, importNode, cloneNode
  • append, appendChild, prepend, before, after, insertBefore
  • insertAdjacentHTML, insertAdjacentText, insertAdjacentElement
  • replaceChildren, replaceChild, replaceWith, remove, removeChild
  • innerHTML, outerHTML, innerText, outerText, textContent
  • setAttribute, setAttributeNS, removeAttribute, removeAttributeNS, toggleAttribute
  • querySelector, querySelectorAll, getElementById, getElementsByTagName, getElementsByClassName, getElementsByName
  • href, src, action, formAction, srcdoc
  • cookie, cookieStore, localStorage, sessionStorage
  • FormData, URL, URLSearchParams, Request, Headers, Response
  • media, Image, Audio, Video, Track, Source, Bitmap, Canvas, Blob, File
  • navigator, postMessage, document, window, globalThis, global, self, this, top, parent, frames, form, element, constructor
  • eval, Function, setTimeout, setInterval

A Content Security Policy is also applied: style-src 'nonce-{random}'; style-src-attr 'none'

Connect with SSH

Link your SSH key, then connect with: ssh [email protected]

30-Day Scoreboard:

This scoreboard reflects solves for challenges in this module after the module launched in this dojo.

Rank Hacker Badges Score