Cross-Site Scripting


CSE 598 ASOW - Fall 2026.

Semantic gaps can occur (and lead to security issues) at the interface of any two technologies. So far, we have seen them happen between:

  • A web application and the file system, leading to path traversal.
  • A web application and the command line shell, leading to command injection.
  • A web application and the database, leading to SQL injection.

One part of the web application story that we have not yet looked at is the web browser. We will remedy that oversight with this challenge.

A modern web browser is an extraordinarily complex piece of software. It renders HTML, executes JavaScript, parses CSS, lets you access pwn.college, and much much more. Specifically important to our purposes is the HTML that you have seen being generated by every challenge in this module. When the web application generated paths, we ended up with path traversals. When the web application generated shell commands, we ended up with shell injections. When the web application generated SQL queries, we ended up with SQL injections. Do we really think HTML will fare any better? Of course not.

The class of vulnerabilities in which injections occur into client-side web data (such as HTML) is called Cross Site Scripting, or XSS for short (to avoid the name collision with Cascading Style Sheets). Unlike the previous injections, where the victim was the web server itself, the victims of XSS are other users of the web application. In a typical XSS exploit, an attacker will cause their own code to be injected into (typically) the HTML produced by a web application and viewed by a victim user. This will then allow the attacker to gain some control within the victim's browser, leading to a number of potential downstream shenanigans.

This challenge is a very first step in this direction. As before, you will have the /challenge/server web server. This challenge explores something called Stored XSS, which means that data that you store on the server (in this case, posts in a forum) will end up being shown to a victim user. Thus, we need a victim to view these posts! You will now have a /challenge/victim program that simulates a victim user visiting the web server.

Set up your attack and invoke /challenge/victim with the URL that will trigger the Stored XSS. In this level, all you have to do is inject a textbox. If our victim script sees three textboxes, we will give you the flag!


DEBUGGING: How do you debug these sorts of attacks? The most common thing to go wrong in this simple scenario is that the resulting post-injection HTML is invalid. Here, the View Source functionality of your browser can help. You can either try launching your attack against the web browser in the DOJO's GUI Desktop (e.g., set up the XSS, then visit with the DOJO's Firefox rather than with /challenge/victim), and View Source, or just use curl and read the result. The result, after you inject your payload, should still be valid (but newly-evil) HTML!

Okay, so injecting some HTML was pretty cool! You can imagine how this can be used to confuse victims, but it gets worse...

In the 1990s, the wise designers of the web invented JavaScript to make websites more interactive. JavaScript lives alongside your HTML, and makes things interesting. For example, this turns your browser into a clock:

<html>
  <body>
    <script>
      document.body.innerHTML = Date();
    </script>
  </body>
</html>

Basically, the HTML <script> tag tells the browser that what is inside that tag is JavaScript, and the browser executes it. I'm sure you can see where this is going...

In the previous level, you injected HTML. In this one, you must use the exact same Stored XSS vulnerability to execute some JavaScript in the victim's browser. Specifically, we want you to execute the JavaScript alert("PWNED") to pop up an alert that informs the victim that they've been pwned. The how of this level is the exact same as the previous one; only the what changes, and suddenly, you're cooking with gas!


DEBUGGING: Here, we need a slightly more advanced approach to debugging. Two main things can go wrong here.

  1. First, you might not be injecting your <script> tag properly. You should check this similar to the debugging path of the previous challenge: bring it up in Firefox and View Source or Inspect Element to make sure it looks correct.
  2. Second, your actual JavaScript might be buggy. JavaScript errors will show up on your Firefox console. Pull up the web development console in the DOJO's Firefox, load the page, and see if anything has gone wrong! If it hasn't, consider resorting to print-debugging inside JavaScript (you can print to the console with, e.g., console.log("wtf").

Like with SQL injection and command injection, sometimes your XSS occurs in the middle of some non-optimal context. In SQL, you have dealt with injecting into the middle of quotes. In XSS, you often inject into, for example, a textarea, as in this challenge. Normally, text in a textarea is just, well, text that'll show up in a textbox on the page. Can you bust out of this context and alert("PWNED")?

As before, the /challenge/victim of this challenge takes a URL argument on the commandline, and it will visit that URL.

In the previous examples, your injection content was first stored in the database (as posts), and was triggered when the web server retrieved it from the database and sent it to the victim's browser. Because the data has to be stored first and retrieved later, this is called a Stored XSS. However, the magic of HTTP GET requests and their URL parameters opens the door to another type of XSS: Reflected XSS.

Reflected XSS happens when a URL parameter is rendered into a generated HTML page in a way that, again, allows the attacker to insert HTML/JavaScript/etc. To carry out such an attack, an attacker typically needs to trick the victim into visiting a very specifically-crafted URL with the right URL parameters. This is unlike a Stored XSS, where an attacker might be able to simply make a post in a vulnerable forum and wait for victims to stumble onto it.

Anyways, this level is a Reflected XSS vulnerability. The /challenge/victim of this challenge takes a URL argument on the commandline, and it will visit that URL. Fool the /challenge/victim into making a JavaScript alert("PWNED"), and you'll get the flag!

Actual XSS exploits try to achieve something more than alert("PWNED"). A very common goal is to use the ability to execute JavaScript inside a victim's browser to initiate new HTTP requests masquerading as the victim. This can be done in a number of ways, including using JavaScript's fetch() function.

This challenge implements a more complex application, and you will need to retrieve the flag out of the admin user's unpublished draft post. After XSS-injecting the admin, you must use the injection to make an HTTP request (as the admin user) to enable you to read the flag. Good luck!


DEBUGGING: This level adds an additional bit of complexity to the injected script: the fetch(). Now, three things can go wrong:

  1. The <script> HTML injection. Again, verify that using View Source or Inspect Element in the DOJO's Firefox. Log in as guest (or modify the script so that you can log in as admin in practice mode) and play around graphically.
  2. The JavaScript itself. Verify this by checking Firefox's JavaScript console for errors and by using print-debugging (to the Firefox console by doing console.log).
  3. The GET request that you'll trigger using fetch() or whatnot. You can, again, debug this in Firefox by looking at the Network tab of the Web Developer Tools. Have the tab open, trigger your attack, and see what's happening with the actual request.

Once an attacker has code execution inside a victim's browser, they can do a lot of things. You've made a GET request in your previous attack, but typically, it's the POST requests that will change application state. This challenge ratchets up the realism: the /publish now needs a POST request. Luckily, fetch supports this!

Go figure out how to POST, and get the flag.

Depending on the attacker's goals, what they might actually be after is the victim's entire account. For example, attackers might use XSS to exfiltrate victim authentication data and then use this data to take over the victim's account.

Authentication data is often stored via browser cookies, such as what happened in Authentication Bypass 2 (but, typically, much more secure). If an attacker can leak these cookies, the result can be disastrous for the victim.

This level stores the authentication data for the logged in user in a cookie. You must use XSS to leak this cookie so that you can, in turn, use it in a request to impersonate the admin user. This exfiltration will happen over HTTP to a server that you run, and everything you need is available via JavaScript's fetch() and its ability to access (some) site cookies.


HINT: By "server that you run", we really mean that listening on a port with nc will be sufficient. Look at the -l and -v options to nc.

An HttpOnly session cookie cannot be read through document.cookie. The browser still attaches that cookie to same-origin requests made by injected JavaScript, and the script can read the responses to those requests. Use the administrator's browser to retrieve protected data and exfiltrate it to a server you control. Run /challenge/server, save your payload, start your listener, and run /challenge/victim.

HTML elements with certain id and name values become named properties on the page's global object. Legacy compatibility makes that behavior useful to ordinary pages, but it also means injected markup can replace values that trusted JavaScript expects to be plain configuration objects. When that script uses the resulting property in a security-sensitive operation, markup alone can redirect its behavior without injecting a new script. Use that behavior to steer the administrator page's trusted code, recover its token, and redeem it for the flag. Run /challenge/server, save your markup, start your listener, and run /challenge/victim.

Every ordinary JavaScript object inherits properties from Object.prototype unless the application deliberately chooses otherwise. An unsafe recursive merge can follow special keys into that shared prototype, making an attacker-controlled value appear on unrelated objects later in the page. When one of those inherited values reaches an HTML sink, prototype pollution becomes DOM XSS. Turn the unsafe merge into code execution in the administrator's origin and exfiltrate the protected data. Run /challenge/server, save your JSON, start your listener, and run /challenge/victim.

HTML sanitization is tied to the parse tree and context that the sanitizer inspected. If an application later concatenates that output into a raw-text wrapper and parses it again, text that was harmless inside an attribute can acquire structural meaning. This parse-sanitize-reparse gap is called mutation XSS. Make the review system's markup-source preview mutate sanitized input into executable markup and exfiltrate the administrator's protected data. Run /challenge/server, save your markup, start your listener, and run /challenge/victim.

A nonce-based Content Security Policy authorizes trusted scripts while rejecting injected script elements and event handlers. With 'strict-dynamic', that trust also follows scripts which an authorized script loads at runtime. This is safe only when every trusted loader keeps attacker-controlled values away from its script source. Find the loader gadget in the administrator's review page, make trusted code load your script, and exfiltrate the protected data. Run /challenge/server, save your markup, start your web server, and run /challenge/victim.

Blocking JavaScript does not make attacker-controlled CSS inert. CSS attribute selectors can test secrets already present in the DOM, while a matching rule can trigger an outbound request through url(). Repeated guesses turn those conditional requests into an oracle that reveals the secret one piece at a time. Recover the administrator's token without JavaScript, redeem it, and get the flag. Run /challenge/server, save your CSS, start your listener, and run /challenge/victim.

An HTML injection can alter how the browser parses markup that follows it even when Content Security Policy blocks JavaScript. Leaving an attribute value unfinished can make later page content part of a resource URL, causing the browser to send that content to another origin. This technique is called dangling-markup injection. Use the administrator's review page to capture its token without JavaScript, then redeem the token for the flag. Run /challenge/server, save your markup, start your listener, and run /challenge/victim.

Frames and windows from different origins cannot directly read each other's pages, so applications often use postMessage() to communicate across that boundary. The receiving page must authenticate both the sender's exact origin and the window that sent the message before returning sensitive data. A hostname-suffix check can mistake an attacker-controlled subdomain for the intended peer, and a permissive reply sends the result back across the boundary. Build a page at http://hacker.challenge.localhost:1337/ that talks to the administrator's dashboard, recover its token, and redeem it for the flag. Run /challenge/server, start your web server, and run /challenge/victim.


30-Day Scoreboard:

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

Rank Hacker Badges Score