May 30, 2026
Cross-Site Scripting (XSS) via Client-Side Filter Bypass
Hey Everyone,
By Abhishek Vishwakarma
7 min read
I am a Security Analyst who enjoys finding weaknesses before the bad guys do.
This is my first public writeup. In this writeup, I will demonstrate a methodology for discovering and exploiting XSS vulnerabilities. I've recreated the vulnerability in a local lab and documented the entire process from discovery to exploitation.
If you are just starting out, I hope this motivates you to document your findings too. Every expert started somewhere.
Let's get into it.
๐ Summary
Cross-Site Scripting (XSS) is one of those vulnerabilities that often hides in plain sight. One of the most common places to look for XSS is input fields especially those that have client-side protections like character filters, paste blocking, or script tag detection. These protections feel secure on the surface but can often be bypassed in ways developers never anticipated.
One such place to look is any input field that:
- ๐ Blocks special characters on keyboard input
- ๐ Blocks paste via JavaScript event handlers
- ๐ Filters script tags only on form submit
If you see any of these protections in place, the next step is to open browser DevTools and check whether the protection is client-side only. If it is it is worth digging deeper.
In one such case, I came across a Location Tag input field that had all three of these protections applied purely on the frontend. Using nothing more than the browser console, I was able to completely bypass all of them without writing a single line of backend code or using any special hacking tool.
The end result was a working XSS payload that exposed session cookies including authentication tokens, user roles, and CSRF tokens exactly what an attacker would need to hijack a victim's account. I reported this finding to the application team through responsible disclosure.
๐ค What Is XSS?
XSS stands for Cross-Site Scripting. It is a vulnerability where an attacker injects a malicious JavaScript script into a web application. When the application displays that value back on the page without sanitizing it, the script runs in the victim's browser.
Think of it like this:
๐ฌ You write your name in a form. The app displays "Hello, [your name]". Now instead of your name, you write a script. The app then runs that script instead of just displaying it.
XSS is ranked in the OWASP Top 10 the most critical web security risks because it can lead to full account takeover.
๐ฏ The Target Application
The application I tested is an asset management platform. For now, let's call it NexusOps this is not the real name of the application. After login, users can register assets using a form. One of the fields in this form is called Location Tag and this is where things got interesting.
This field had the following protections:
| Protection | How it was implemented |
|-----------------------|---------------------------------------|
| No special characters | Characters stripped on keyboard input |
| No paste allowed | Paste event blocked via JavaScript |
| No script tags | A single <script> tag was blocked on form submit || Protection | How it was implemented |
|-----------------------|---------------------------------------|
| No special characters | Characters stripped on keyboard input |
| No paste allowed | Paste event blocked via JavaScript |
| No script tags | A single <script> tag was blocked on form submit |On the surface this looks secure. A normal user cannot type <, >, or any special character. They cannot paste anything either.
๐ Reconnaissance
The first thing I did was capture the request using Burp Suite to understand how the application was sending data to the server.
This is where I found something very interesting the parameters in the request were encrypted. The values being sent to the server were not in plain text. This meant:
- โ I could not directly manipulate the
location_tagparameter from Burp Suite - โ The server-side was not visible or accessible for direct injection
- โ Tampering with the encrypted parameter would likely break the request entirely
This is a common technique used by developers to prevent parameter tampering. At first glance it seems like a strong protection if you cannot read or modify the parameter on the server side, how do you inject anything?
๐ก Key realisation: If the server-side parameter is encrypted and cannot be manipulated directly, the only remaining attack surface is the client side specifically the input field itself before the value gets encrypted and sent.
๐ต๏ธ Inspecting the Field
I right-clicked the Location Tag field and clicked Inspect Element. In the DevTools Inspector panel I could see the actual HTML code of the input field:
<input id="location_tag" type="text" name="location_tag"
placeholder="e.g. DC-West-3A" autocomplete="off"
onpaste="return blockPaste(event)"
oninput="validateLocationTag(this)"><input id="location_tag" type="text" name="location_tag"
placeholder="e.g. DC-West-3A" autocomplete="off"
onpaste="return blockPaste(event)"
oninput="validateLocationTag(this)">This told me three important things:
- ๐ฉ The field had
onpaste="return blockPaste(event)"paste was blocked using a JavaScript event handler - ๐ฉ It had
oninput="validateLocationTag(this)"every keystroke was being validated by a JavaScript function - ๐ก Most importantly both protections were JavaScript-based and client-side only
Key insight: The server-side was protected by encryption. The client-side was protected only by JavaScript. JavaScript can be bypassed directly from the browser console.
โก Bypass Step 1 โ Injecting via Console
I opened the browser console (F12 โ Console tab) and ran this command:
document.getElementById('location_tag').value = '<script>alert(1)</script>';document.getElementById('location_tag').value = '<script>alert(1)</script>';What this does: Instead of typing into the field through the keyboard, I directly set the field's value using JavaScript. This completely skips the keydown event listener that strips special characters because setting .value programmatically does NOT trigger keyboard events
The field now showed <script>alert(1)</script> โ the special characters went in without being stripped. โ
๐ซ First Attempt โ Blocked by Submit Filter
I clicked Register Asset to submit the form.
The security log on the right showed:
โ CLIENT FILTER: Single <script> tag detected โ BLOCKED.
Hint: try two consecutive tags to bypass this check.โ CLIENT FILTER: Single <script> tag detected โ BLOCKED.
Hint: try two consecutive tags to bypass this check.The application had a second layer of protection a filter on the submit button that checked for a <script> tag in the value. The filter used this logic:
const singleTagOnly = /^<script>[^<]*<\/script>$/i;const singleTagOnly = /^<script>[^<]*<\/script>$/i;This regex blocks a single <script>...</script> tag. It only matches when the entire value is exactly one script tag.
๐ Bypass Step 2 โ Double Tag Technique
I noticed the filter only checks for a single script tag. So I tried injecting two consecutive script tags:
document.getElementById('location_tag').value = '<script>alert(1)</script><script>alert(1)</script>';document.getElementById('location_tag').value = '<script>alert(1)</script><script>alert(1)</script>';When the form was submitted, the regex check ran:
Does the value match: ^<script>[^<]*</script>$ ?Does the value match: ^<script>[^<]*</script>$ ?The value was <script>alert(1)</script><script>alert(1)</script> โ this does NOT match the pattern because it has two tags, not one. So the filter passed it through. The bypass worked. โ
๐ช Escalation โ Cookie Theft
A simple alert(1) only proves the vulnerability exists. In a real attack, the goal is to steal the victim's session cookies to take over their account.
After logging in, the application sets these cookies:
- ๐
session_idโ JWT authentication token - ๐
auth_tokenโ API access key - ๐ค
user_roleโ privilege level (superuser) - ๐ง
user_emailโ logged-in user's email - ๐ข
orgโ organisation name - ๐ก๏ธ
csrf_tokenโ CSRF protection token
I crafted a cookie theft payload and injected it using the same bypass:
document.getElementById('location_tag').value = '<script>alert(document.cookie)</script><script>alert(document.cookie)</script>';document.getElementById('location_tag').value = '<script>alert(document.cookie)</script><script>alert(document.cookie)</script>';I clicked Register Asset and the alert popup appeared showing all the raw session cookies directly. The alert popup displayed the raw session cookies exactly what an attacker would see after a successful XSS attack. These cookies contain sensitive authentication data that could be used to hijack the victim's account without ever knowing their password. ๐ฑ
๐ Full Attack Chain
Here is the complete attack from start to finish:
๐ Step 1: Capture request in Burp Suite
โ Parameters are encrypted โ server-side manipulation not possible
โ
๐ก Step 2: Shift focus to client-side input field
โ Inspect the field HTML in DevTools
โ
โก Step 3: Set field value directly via browser console
โ Bypasses keydown filter (no keyboard event fired)
โ
๐ซ Step 4: Submit the form
โ Single tag filter BLOCKS the payload
โ
๐ Step 5: Use double tag technique
โ Regex only matches single tag โ double tag slips through
โ
๐ป Step 6: Application renders raw HTML
โ Script tags execute in browser
โ
๐ช Step 7: document.cookie exposed
โ Session tokens, auth tokens stolen
โ
โ ๏ธ Step 8: Account takeover possible๐ Step 1: Capture request in Burp Suite
โ Parameters are encrypted โ server-side manipulation not possible
โ
๐ก Step 2: Shift focus to client-side input field
โ Inspect the field HTML in DevTools
โ
โก Step 3: Set field value directly via browser console
โ Bypasses keydown filter (no keyboard event fired)
โ
๐ซ Step 4: Submit the form
โ Single tag filter BLOCKS the payload
โ
๐ Step 5: Use double tag technique
โ Regex only matches single tag โ double tag slips through
โ
๐ป Step 6: Application renders raw HTML
โ Script tags execute in browser
โ
๐ช Step 7: document.cookie exposed
โ Session tokens, auth tokens stolen
โ
โ ๏ธ Step 8: Account takeover possible๐ง Why This Happened โ Root Cause
The vulnerability exists because the developers made one fundamental mistake:
โ They trusted the client (browser) to enforce security.
Here is why each protection failed:
| Protection | Why it failed |
|------------------------------|--------------------------------------------------------------------------|
| keydown blocks special chars | Only fires on real keyboard input. Console .value assignment bypasses it |
| onpaste blocks paste | Only fires on paste event. Console assignment bypasses it |
| Single script tag regex | Only matches one tag. Two tags bypass the pattern |
| No server-side check | Raw value was rendered as HTML without any server sanitization || Protection | Why it failed |
|------------------------------|--------------------------------------------------------------------------|
| keydown blocks special chars | Only fires on real keyboard input. Console .value assignment bypasses it |
| onpaste blocks paste | Only fires on paste event. Console assignment bypasses it |
| Single script tag regex | Only matches one tag. Two tags bypass the pattern |
| No server-side check | Raw value was rendered as HTML without any server sanitization |๐ก Golden rule of web security: Never trust client-side validation. Always validate and sanitize on the server.
๐ฅ Impact
If this vulnerability existed in a real production application:
- ๐ An attacker could steal any logged-in user's session cookies
- ๐ With the
session_idandauth_token, they could log in as that user from any device without knowing the password - ๐ With
user_role=superuser, they would have full admin access - ๐ก๏ธ The
csrf_tokentheft also breaks CSRF protection - โ ๏ธ This could lead to complete account takeover
๐ง Recommendations โ How to Fix
- ๐ก๏ธ Server-side sanitization (Most important) Never render raw user input as HTML. Always sanitize on the server:
Input: <script>alert(1)</script>
Output: <script>alert(1)</script>Input: <script>alert(1)</script>
Output: <script>alert(1)</script>- ๐ช HttpOnly cookies Mark session cookies as HttpOnly so JavaScript cannot read them:
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=StrictSet-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Strict- ๐ Content Security Policy (CSP) Add a CSP header to block inline script execution entirely:
Content-Security-Policy: default-src 'self'; script-src 'self';Content-Security-Policy: default-src 'self'; script-src 'self';-
๐ช Strong server-side regex If you must filter on submit, do it on the server and catch all variations not just a single tag format.
-
๐ฆ Use a sanitization library Libraries like DOMPurify strip all dangerous HTML before rendering:
const clean = DOMPurify.sanitize(userInput);const clean = DOMPurify.sanitize(userInput);๐ Conclusion
This writeup demonstrates how a seemingly secure input field can be completely bypassed using nothing more than the browser's built-in developer tools. No special software, no proxies, no advanced hacking knowledge was needed.
The attack worked because:
- The encrypted request parameters created a false sense of security
- All input validation was implemented on the client side only
- The submit filter used a naive regex that only matched a single pattern
- The server rendered raw user input as HTML without any sanitization
Thank you for reviewing this Writeup!