September 13, 2026
Exotic Sources: window.name, History State, Storage, Cookies, Referrer
Attacker-controlled, invisible to every server-side defence, and unguarded because developers do not think of them as input.

By Nitin yadav
7 min read
Hello, I am Nitin.
Day thirteen. Today is about the input channels most hunters never test, because they are not visible in Burp's request list.
Here is the mental shift. When you learn web hacking, you learn to think of input as "things I put in the request." Query parameters, body fields, headers, cookies. You watch them in your proxy, you tamper with them, you send them again.
But a browser gives JavaScript several inputs that never appear in any request at all. They live entirely on the client. Your proxy will not show them. The target's WAF cannot inspect them. Their access logs will never contain them. And their developers frequently do not think of them as input, which is exactly why the code that reads them is unguarded.
That combination โ attacker-controlled, invisible to defences, and unguarded โ is why this is one of the highest-value days in the whole month.
Source one: the fragment
You met this on day eight. It deserves the top slot, so here is the practical detail.
Everything after the hash in a URL is never transmitted. The browser strips it before sending. Open your proxy, load a URL with a fragment, and look at the request line โ it is not there.
Finding fragment sinks
grep -rnE "location\.hash|location\['hash'\]|\.hash\b" pretty/grep -rnE "location\.hash|location\['hash'\]|\.hash\b" pretty/For each hit, read what happens to the value. The classic vulnerable pattern:
// scroll-to-section feature, present on thousands of sites
const target = location.hash.slice(1);
document.querySelector('#' + target).scrollIntoView();// scroll-to-section feature, present on thousands of sites
const target = location.hash.slice(1);
document.querySelector('#' + target).scrollIntoView();And the version that actually executes:
// tab or panel routing
const tab = decodeURIComponent(location.hash.slice(1));
document.getElementById('panel').innerHTML = tabContent[tab] || tab;// tab or panel routing
const tab = decodeURIComponent(location.hash.slice(1));
document.getElementById('panel').innerHTML = tabContent[tab] || tab;Practical notes
- Decoding varies. Some code reads the raw hash, some decodes it. Test both encoded and raw payloads.
- Hash routers interfere. If the app uses hash-based routing, your payload may be consumed as a route. Look at how the router splits it and place your payload in a segment it passes through.
- Reload matters. Changing only the fragment does not reload the page. If the reading code runs on load, you need a full reload, or a
hashchangehandler must exist.
Always say in the report that the payload lives in the fragment and therefore never reaches their infrastructure. It changes how the finding is understood, and it is true.
Source two: window.name, the smuggling channel
This one is genuinely strange and it is my favourite source in the list.
Every browsing context has a name property. It is a plain string, you can set it to anything, and here is the part that matters: it survives navigation, including navigation to a completely different origin.
Set it on your page, navigate to the target, and the target's JavaScript reads a value you chose.
Why that is powerful
Three properties no other source has together:
- No length limit that matters. URLs get truncated by proxies, logged, and sometimes filtered on length. A name can hold a very large payload.
- Nothing in the URL. The victim's address bar shows a clean target URL with no payload in it. Nothing to look suspicious, nothing to filter.
- Crosses origins. The value you set on your domain is readable by the target's script on their domain.
Try it right now
In the console of any page:
window.name = 'nitn4041x';
location = '<https://example.com>';window.name = 'nitn4041x';
location = '<https://example.com>';When the new page loads, check window.name in the console. Your string is still there.
Finding the sinks
grep -rnE "window\.name|self\.name|top\.name" pretty/grep -rnE "window\.name|self\.name|top\.name" pretty/The pattern to hope for:
// legacy cross-domain data passing, still common in older SDKs
if (window.name) {
const data = JSON.parse(window.name);
container.innerHTML = data.content;
}// legacy cross-domain data passing, still common in older SDKs
if (window.name) {
const data = JSON.parse(window.name);
container.innerHTML = data.content;
}This idiom exists because before cross-window messaging was widely supported, the name property was how people passed data between origins. That code never got removed. It is exactly the kind of decade-old plumbing that nobody reviews.
The delivery page
Your proof of concept is a few lines of logic on a page you control:
window.name = JSON.stringify({
content: '<img src=x onerror=alert(document.domain)>'
});
location = '<https://target.com/vulnerable-page>';window.name = JSON.stringify({
content: '<img src=x onerror=alert(document.domain)>'
});
location = '<https://target.com/vulnerable-page>';The victim visits your page, gets redirected to the target, and the payload arrives without ever appearing in a request.
Combine it with other sources too: the name property is a good way to smuggle a payload too long for a fragment, or to hold a second stage that a short injected payload fetches.
Source three: storage, the two-step bug
Local and session storage are attacker-influenced far more often than people assume, and they create a pattern worth naming: one feature writes, a different feature reads.
The writer and the reader are usually in different files, written by different people, at different times. The writer thinks it is storing trusted data. The reader thinks it is reading trusted data. Neither validates.
How you get to control what is stored
You rarely write to storage directly. You get there through a first step:
- A URL parameter the app saves โ a theme, a locale, a referral code, a last-search term, a return path
- A value from a cross-window message that gets persisted (day 10 plus this equals a full chain)
- A field on your own profile that the app caches client-side
- A cookie you can set on a subdomain that the app copies into storage
Finding it
grep -rnE "localStorage|sessionStorage" pretty/grep -rnE "localStorage|sessionStorage" pretty/Then split the hits into writers and readers, and look for a key that appears in both lists. That key is your candidate.
Watch the storage live while you use the application:
// paste before interacting, then browse normally
(() => {
const s = Storage.prototype.setItem;
Storage.prototype.setItem = function (k, v) {
console.log('%c[storage write]', 'color:#C0392B', k, '=', v);
return s.apply(this, arguments);
};
})();// paste before interacting, then browse normally
(() => {
const s = Storage.prototype.setItem;
Storage.prototype.setItem = function (k, v) {
console.log('%c[storage write]', 'color:#C0392B', k, '=', v);
return s.apply(this, arguments);
};
})();Now every write prints. Use the app for five minutes and you will have a complete map of what gets stored and from where.
Practical test
Set a marker in storage manually and reload:
localStorage.setItem('theme', 'nitn4041x');
location.reload();localStorage.setItem('theme', 'nitn4041x');
location.reload();Then search the DOM for the marker. If it appears rendered, find the write path and check whether you can drive it from a URL. If you can, that is a full chain.
Source four: history state
pushState and replaceState take a state object that persists in the history entry and is readable later as history.state, usually on a popstate event.
grep -rnE "history\.(state|pushState|replaceState)|popstate" pretty/grep -rnE "history\.(state|pushState|replaceState)|popstate" pretty/Less common than the others, but when it hits it is very clean, because state objects are structured data and developers treat them as trusted internal plumbing. Test by pushing your own state and triggering a back navigation:
history.pushState({ title: '<img src=x onerror=alert(document.domain)>' }, '', location.href);
history.pushState({}, '', location.href);
history.back();history.pushState({ title: '<img src=x onerror=alert(document.domain)>' }, '', location.href);
history.pushState({}, '', location.href);
history.back();Source five: the referrer
document.referrer is the URL of the page that linked to the target. You control that page, and you control its URL, so you control the string.
grep -rn "document\.referrer" pretty/grep -rn "document\.referrer" pretty/The vulnerable pattern is almost always analytics or a "back to previous page" link:
backLink.href = document.referrer; // navigation sink
document.getElementById('src').innerHTML = 'From: ' + document.referrer;backLink.href = document.referrer; // navigation sink
document.getElementById('src').innerHTML = 'From: ' + document.referrer;To exploit, host a page at a URL containing your payload and link to the target from it. Note that referrer policy affects how much of your URL is sent โ many sites now send origin only, which kills this. Check the target's referrer policy header before spending time here.
Source six: cookies
Cookies feel server-side, but JavaScript reads them, and any subdomain can write a cookie for the parent domain. If you have XSS or content control on any subdomain, or a subdomain takeover, you can set a cookie that the main application's JavaScript reads.
grep -rn "document\.cookie" pretty/grep -rn "document\.cookie" pretty/This is the bridge that makes an otherwise low-value subdomain finding into something serious, and it is covered further in day 27 under cookie tossing and in day 29 under subdomain trust.
The workflow, in order
One. Pull the bundles for the target page (day nine) and run all six greps at once:
grep -rnE "location\.hash|window\.name|self\.name|localStorage|sessionStorage|history\.state|popstate|document\.referrer|document\.cookie" pretty/ > exotic-hits.txt
wc -l exotic-hits.txtgrep -rnE "location\.hash|window\.name|self\.name|localStorage|sessionStorage|history\.state|popstate|document\.referrer|document\.cookie" pretty/ > exotic-hits.txt
wc -l exotic-hits.txtTwo. For each hit, read fifty lines around it and answer one question: does this value reach a sink from day eight?
Three. Install the storage hook in the console and use the application normally for five minutes. Read what got written and from where.
Four. Test each source with a marker rather than a payload, exactly as in day two. Marker in, search the DOM, see where it lands.
Five. Only then build the payload, matched to the context you found.
Six. Prefer the fragment or the name property for the final proof of concept, because those make the strongest severity argument.
Impact ladder
- Informational โ source read but never reaches a sink.
- Low โ source reaches a sink you can only trigger against yourself, with no cross-user delivery.
- Medium โ referrer or storage chain requiring several steps from the victim.
- High โ fragment or name-property source reaching an execution sink, firing on a single page visit, on an authenticated origin.
- Critical โ a storage or cookie chain where the write is driven by another user's action or by a subdomain you control, so no link is needed at all.
Make the invisibility argument explicitly. A finding where the payload never touches the server means their WAF, their logging, their rate limiting, and their server-side validation are all structurally unable to help. Triage teams weigh that, and it is an accurate statement rather than a rhetorical one.
Conclusion โ steal this checklist
- Browsers give JavaScript inputs that never appear in any request. Your proxy will not show them and no server-side control can inspect them.
- Six to test every time: fragment, name property, local and session storage, history state, referrer, cookies.
- The fragment is never transmitted. Say so in the report; it changes how the finding is understood.
- The name property survives cross-origin navigation, has no practical length limit, and leaves the address bar clean. Best smuggling channel available.
- Storage bugs are two-step: one feature writes, another reads. Find a key that appears in both the writer and reader lists.
- Hook the storage setter in the console and browse for five minutes to map every write.
- History state is rarer but very clean when it hits, because developers treat state objects as trusted plumbing.
- Check the referrer policy before investing in referrer sinks โ origin-only policies kill them.
- Any subdomain can write a cookie for the parent domain. That is what upgrades a minor subdomain issue into a serious one.
- Marker first, payload second, exactly as in day two.
- Prefer fragment or name-property delivery in the final proof for the strongest severity argument.
Tomorrow: framework escape hatches โ the deliberate holes in React, Vue, Angular, Svelte, and Next.js, and how to find where developers opted out of safe defaults. That closes week two.
If you Love reading my blogs. Check my Youtube Channel too.