Sent on

Building the XSS Challenges

Hey there!

As I mentioned yesterday, I wanted to talk a bit about building the challenges within Practical Laravel Security - specifically the XSS challenges. It's something I always find interesting to learn about, but feel free to skip this email if it's not your thing. 🙂

The centerpiece of the course is the interactive Attack Challenges. I want you to learn how each attack works by performing the attacks yourselves. I believe this is the best way to learn about web security, because it allows you to look at a piece of code you're working on and see the potential vulnerabilities. Once you start to understand how vulnerabilities are introduced, you'll start noticing them and you'll also know how to fix them.

With these interactive challenges, I knew some of them were going to be easy. XSS for instance is all contained within the browser, so there is zero risk to the platform. SQL Injection (SQLi) can also be sandboxed within a read-only database. The challenge will come with vulnerabilities like Remote Code Execution (RCE) and PHP Object Injection (Deserialisation attacks). That's why I'm starting with XSS!

With all of this in mind, I came up with some requirements for the challenges:

  1. Run within a sandboxed iframe, to protect the course app.

  2. All vulnerabilities in a standalone app with no special access to anything. (i.e. no API keys, no privileged network, no secrets or sensitive user state).

  3. The vulnerable app should only be accessible by course users.

  4. Separate domains for everything to prevent Same-Site crossover.

  5. Separate infrastructure to prevent intrusion and escalation beyond the expected bounds of the intentional vulnerabilities.

  6. Challenge completion should be automatically detected and recorded.

As you can expect, these requirements have forced some rather interesting design choices. I couldn't simply use Laravel's Auth system or pre-shared keys and signed links... I had to get a lot more creative.

Here's what I've done...

Sandboxed <iframe>

I wanted to include the attacks within the course screen, which called for a <iframe> to embed the actual challenge page. Now while I want the user to exploit vulnerabilities within the iframe, I also want to protect the parent course app from anything they can do. It turns out this is relatively easy. modern browsers already do a lot by default to sandbox iframes, and they also include a sandbox attribute that allows you to define exactly what the browser allows the iframe to do (check out the docs).

The resulting frame I came up with looks like this:

<iframe src="" sandbox="allow-forms allow-modals allow-same-origin allow-scripts"></iframe>

Note, I had to include allow-same-origin to allow cookies to be set so they can be stolen in our XSS challenges.

Turns out that the iframe was the easy bit.

Unique Subdomains

I wanted to protect the vulnerable app from anyone accessing it, but without auth sessions, I needed to go down the token route. I originally went for a query string based token, but realised that was too easily dropped in various attacks and went for a different approach: randomly subdomain.

Linking this up was trivial, thanks for Laravel's subdomain routing support. When the vulnerable app receives a request, it extracts the subdomain token and sends it to an unauthenticated API endpoint on the course app to verify it's a legitimate token. The course app returns a single value: the XSS flag (we'll get to that shortly).

I was initially worried about how to protect the API endpoint on the course app, as using a pre-shared key is pointless since anyone compromising the vulnerable app would gain access to it. However I realised that it didn't need to be authenticated since all it does is validate the subdomain and provide the flag - both of which are easily obtainable through the vulnerable app. If you know the subdomain token, you already have access to everything.

I built and shipped it, and it seemed to work nicely.

But then I noticed this when I visited it directly...

It doesn't appear to be affecting the embedded iframe, but it's definitely something I need to keep an eye on!

Stealing the Cookie

The first XSS challenge is to steal a cookie value and send it to a target endpoint. This is relatively trivial with a completely unprotected XSS vulnerability, but I felt like a good place to start.

Remember the XSS flag I mentioned before that the course app reports? This is stored in the vulnerable app and set as a cookie value when the challenge page is loaded. Now by default Laravel stores cookies as HTTPOnly, so I had to override that and SameSite, to get it to load within the frame. But with that done, the cookie was now accessible.

All the user needs to do is make a GET request to the target endpoint (the course app with a different domain) and include the contents of document.cookie. The endpoint takes the flag from the cookie values, finds the user it's associated with, and then marks them down as having completed the challenge!

Now, this is a manufactured challenge when Laravel protects its session cookies significantly better than this by default. However it felt like a great starting point to show the concept and prepare you for trickier challenges.


I was expecting setting up the XSS challenges to be fairly simple, beyond getting the infrastructure in place. I have some more XSS challenges that I need to build out today and get working, but I'm expecting them to come together fairly easily.

The problem will be expanding this approach for other vulnerabilities. As I said above SQLi should be trivial too, and some other things like missing authorisation. However I also want to include interactive attacks like user enumeration, credential stuffing, etc. So look forward to hearing about those challenges!

One final note, I made the decision to not apply any non-essential styling or Javascript to the vulnerable app. This is to reduce any visual noise or UI complexity - so you can see exactly what's going on in attacks like XSS, rather than figuring out if the styling is getting in the way. I'll introduce elements as they are needed, but we'll stick to the essentials.

Presale Reminder

Don't forget that the presale discount will be ending when Early Access is launched. I'll be bumping the price up a bit, although not up to the full price.

If you haven't signed up yet, you can still get the presale price of $249 USD!

Get the Presale!

Please reach out if you're interested in teams, student, or PPP pricing.