Cookies envenenadas: cuando el "último producto visto" esconde un DOM-based XSS

Cookie manipulation via the DOM

🔍 An XSS that isn’t where you’d look for it

Today I want to share a very instructive case of DOM-based cookie manipulation, a variant of XSS that doesn’t jump out at you during a first pass over the requests, because the problem isn’t in a parameter reflected directly in the HTML, but in a cookie that the page’s own JavaScript builds from data the user controls.

This type of vulnerability often shows up in e-commerce-style “convenience” features: remembering the last product visited, the last filter applied, the last search performed… Any feature that stores “something you did” and later reuses it to render a link or a message is a candidate for review.

🕵️ Scenario

While browsing an online store, every time we visit a product page we notice that a cookie is created (or updated) in the browser:

Cookie: session=xxxxxxxxxxxxxxxxxxxxxxxx; lastViewedProduct=https://tienda.ejemplo.com/product?productId=2

And when we go back to the main page, a new link appears:

<a href='https://tienda.ejemplo.com/product?productId=2'>Last viewed product</a>

At first glance it looks harmless: the application simply remembers the URL of the last product and shows it as a quick shortcut. The problem appears as soon as we ask ourselves where that cookie gets its value from. If, when visiting a product, the client-side script builds lastViewedProduct from window.location.href or a similar attribute, with no encoding or validation whatsoever, that cookie is actually an almost literal reflection of the URL the attacker controls.

💡 Step 1 — Confirming the injection in the cookie

The first step is to check whether we can “break out” of the HTML context where that cookie later gets dumped. If the page that prints the link does something like:

<a href='COOKIE_VALUE'>Last viewed product</a>

without escaping single quotes or special characters, we can close the href attribute and the <a> tag itself by injecting our own HTML inside the URL value:

https://tienda.ejemplo.com/product?productId=3&'><h1>hola</h1>

If, after visiting that URL and going back to the home page, we see the <h1> rendered on the page, the injection is confirmed: the lastViewedProduct cookie is being generated with no sanitization whatsoever from the current URL, and its value is later dumped into the DOM unencoded.

💡 Step 2 — From broken HTML to JavaScript execution

Once we’ve confirmed we can inject arbitrary markup, the natural next step is to replace the <h1> with a vector that executes JavaScript:

https://tienda.ejemplo.com/product?productId=3&'><script>print()</script>

print() is used as a harmless, easily verifiable proof of concept (it triggers the browser’s print dialog) instead of a payload that exfiltrates data — especially advisable in lab environments or controlled pentests.

🚫 The nuance that complicates the attack: two steps, two pages

Here’s the most interesting part of this case. The flaw has a quirk: the cookie gets poisoned when visiting the product page, but the payload only executes when visiting the main page, which is where that cookie is read and dumped into the DOM. A single <script> or a single request isn’t enough: it takes chaining two separate navigations by the victim, in the right order.

To force that sequence without relying on the victim clicking twice on their own, you can use your own exploit server (common in training labs like Web Security Academy) and set up an intermediate page with an <iframe> that does the work in two stages:

<iframe
  width="600" height="600"
  src="https://tienda.ejemplo.com/product?productId=3&'><script>print()</script>"
  onload="if(!window.x){this.src='https://tienda.ejemplo.com/'; window.x=1}"
></iframe>

The logic is as follows:

  • First iframe load: points directly to the product page with the payload injected into the URL. This makes the victim’s browser generate/update the lastViewedProduct cookie, already “poisoned” with our <script> inside it.
  • onload: as soon as that first load finishes, the event itself changes the iframe’s src to the main page.
  • Second iframe load: now the homepage reads the (already contaminated) cookie and dumps it unescaped into the “Last viewed product” link, executing the <script>.
  • The window.x flag prevents onload from firing in an infinite loop, since the iframe’s second load also fires its own onload event.

If the iframe pointed straight at the homepage, the payload would never get to execute: the cookie wouldn’t be contaminated yet at that first instant. The order of the two navigations isn’t a minor detail — it’s the key to the entire attack.

✅ Best practices to avoid falling into this

  • Never trust document.cookie, window.location, or any client-controlled data when dynamically generating HTML. Any data coming from the URL, a form, or a cookie must be treated as untrusted input, whether or not it came from a server request.
  • Always encode when inserting into the DOM. Use textContent instead of innerHTML whenever possible, or attribute/HTML encoding functions when the content must be inserted as markup.
  • Don’t build “convenience” cookies from full URLs without validation. If only the productId is needed, store the numeric identifier and rebuild the URL on the server — never persist the raw URL exactly as it arrives from the browser.
  • Content-Security-Policy header as an extra layer: although it doesn’t replace proper sanitization, it greatly limits the impact if an inline <script> does slip through.
  • Sensitive cookies with HttpOnly when they don’t need to be read by JavaScript, to reduce the attack surface for theft via XSS — even though this particular type of flaw (manipulation, not direct theft) doesn’t depend on that flag.

🔐 Conclusion

What makes this case interesting isn’t the XSS technique itself — a classic injection due to missing escaping — but where the problem lives: not in a server response, but in client-side JavaScript that blindly trusts the current URL to build a cookie, and in another part of the application that trusts that cookie just as blindly to render HTML. When analyzing HTTP requests “shows nothing unusual,” it’s worth also looking at what the browser’s JavaScript does with the data it already has in front of it: cookies, location, referrer, postMessage… The DOM is attack surface too.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top