Here I will analyze all three types of XSS in web applications, not retelling OWASP wikis (the hundredth time it makes no sense to rewrite the stove), but through the analysis of specific HTTP queries, server responses and DOM behavior. I will show how the search for XSS vulnerabilities on a real pentest looks, which event-handlers bypass the WAF signatures right now and why even DOMPurify - the gold standard for sanitaryization - turned out to be leaky through Prototype Pollution (CVE-2024-45801, CVSS 7.3 HIGH).
Three types of cross-site scripting through the prism of HTTP requests
Russian-language materials usually describe three types of XSS in abstract: “reflected returns in response”, “storey stored on the server”, “DOM-based works in the browser”. Formally true, but it is useless for the pentester - we need a discussion at the level of specific requests and points of implementation.
Reflected XSS attack - from GET-parameter to execution
Reflected XSS occurs when the value from the request (GET parameter, POST, HTTP header) gets into the server’s response without processing. The key word is “context of insertion”. The same input behaves completely differently depending on where it lands in HTML.
Let's say there is a search on the target site: GET /search?q=test. The server returns the line Results for your search:: test within the tag <h2>. The first step is to throw through Burp Repeater value like aaa"'<>{} and see what symbols are passing. If you see unedited <> - it's an HTML context, and the classic vector will work through the tag.
But more often it is different. The value gets inside the attribute: <input value="sissejuhatus">. Here <script> useless - you must first get out of the attribute through ">, and then implement your tag. Peyload "><img src=x onerror=alert(1)> closes the attribute, closes the tag and creates a new element with the event handler.
The third context is JavaScript. If the input is included in the structure var x = 'ВВОД';, you need to get out of the line: ';alert(1)//. Each context requires its own pelohad, and that is why blind phases without analyzing the answer is a waste of time.
According to YesWeHack, XSS reflects most often found in search forms, login forms and URLs. When using reflect XSS, the attacker forms a URL with a paced and delivers it to the victim through phishing. If the session for a book without a flag HttpOnly, simple fetch c document.cookie in the parameter sends the session token to the attacker's server. Direct path to the hijacking of the session via XSS (Steal Web Session Cookies, T1539 on MITRE ATT&CK).
Stored XSS Operating - Persistent Vector
Stored XSS is the most dangerous type because the peyload is stored on the server and works at each visitor to the infected page. The introduction of a malicious script goes through the fields that fall into the database: comments, user profiles, support labels, file names.
In practice, stored XSS exploits look like this. The attacker finds a field that takes input and displays it to other users. Throws there a tag with the processor of the event - for example, <img src=x onerror=alert(document.domain)>. If when you view a page with another user, the browser draws this tag and tries to download a non-existent image - it works onerror, and JavaScript injection is executed in the context of the victim session.
A separate subspecies is blind XSS. Peyloade is sent through the feedback form or ticket and works only in the admin, when the employee opens the application. The attacker does not see the result immediately, so he uses an external callback server (Burp Collaborator or its own) to confirm the trigger. Blind XSS is especially valuable for bug bounty - it affects privileged users and is often rated as high or critical. I am on the same project through blind XSS in the support label pulled the session of the administrator - the application hung in line for three days, and all this time the peload waited patiently.
DOM-based XSS is different in that the server response can be completely secure – the vulnerability lies in the client JavaScript, which takes data from a controlled source (source) and inserts them into a dangerous receiver (sink).
Typical source: location.hash, location.search, document.referrer, window.name, postMessage. Typical: sinks innerHTML, outerHTML, document.write, eval(), setTimeout() with string argument, jQuery.html().
The DOM-based XSS search method is reduced to two steps. The first is to find in the sink code. In DevTools on the Sources tab innerHTML, eval and other dangerous methods. The second is to see where the data comes from. If the chain from source to sink does not contain sanitation, it is DOM-based XSS.
A specific cross-site scripting example: the script takes the value from window.location.hash through decodeURIComponent, then insert it through innerHTML in the item on the page. Transition to the URL https://target.com/#<img src=x onerror=alert(1)> leads to the execution of the code without any interaction with the server. WAF, server validation - are powerless, because the peyloade does not leave the browser.
In React applications, a similar problem arises when using dangerouslySetInnerHTML with user input. In Angular - when bypassing DomSantiizer through bypassSecurityTrustHtml. Modern frameworks protect against XSS by default, but the developers deliberately disable the protection for the sake of “flexibility” (and then wonder where they came from).
Search XSS Vulnerabilities: Step-by-step method of pentest
Adjustments to the environment: Burp Suite Community/Pro (actual version), browser with customized proxy (FoxyProxy or system proxy on 127.0.0.1:8080), access to the target application with a test resolution. For DOM-analysis - Chrome DevTools. For automated phases - wordlist with XSS-dipal (recommended SecLists xss-payload-list.txt - to start with enough eyes).
The first step is to the attack surface of the attack. I proxy all traffic through Burp, pass through the appendix, fill out all forms, click all links. In Target > Site map I see a complete parameter map. Each parameter that is reflected in the response is a potential point for XSS. Each parameter that is saved and displayed later is a candidate for the XSS store.
The second step is manual review of the context. For each reflected parameter, I send a test line with special symbols to Repeator: '"<>(){}. I see the answer - that is shielded, what is left. This determines the attack vector. If <> screened, but '" no, and the input falls into the attribute - you can get out of the attribute and use the event handler without creating a new tag: " onfocus=alert(1) autofocus=". Do not rush to phase immediately - first understand the context.
Step three is the fan-the-facing event-handlers. If the standard onerror, onclick, onload blocked WAF, sorting less well-known handlers. To do this, I use PortSwigger XSS Cheat Sheet (an actual version of 2026) - there are dozens of event-landlers by one that require and do not require user interaction. onanimationend, ontransitionend, onscrollend, onbeforematch - all these are real ways to trigger JavaScript without a click.
Step four: DOM check. I open DevTools > Sources, looking for keywords innerHTML, document.write, eval, $.html(. For each found, I monitor the data source. If the source is controlled (hash, query, postMessage) - testing the introduction through it.
Step five - check of the store XSS. Insert peyloade in each field that is saved: username, biography, comment, file name when downloading. Then I check how this input is displayed - in the personal account, in the admin, in email notifications. Blind XSS checks via callback on Burp Collaborator: I implement <img src=https://COLLABORATOR_ID.burpcollaborator.net> and awaiting HTTP circulation.
In addition to the exotic event-handlers, according to the PortSwigger XSS Cheat Sheet, the following approaches to bypass work:
• CSS-animation as a trigger: onanimationstart, ontransitionendand their webkit versions are triggered without user interaction in the presence of CSS rules with @keyframes
• Consuming tags: unclosed tags that “absorb” part of the page and change the context of parsing
• Encoding: HTML-entities (onerror), Unicode-escaped sequence in JavaScript, double URL-encode to bypass server decoders
• Framework-specific: in Vue.js - {{constructor.constructor('alert(1)')()}}for template injection; in AngularJS - sandbox escape through $eval
CVE-2024-45801 - when DOMPurify stops defending
DOMPurify from Cure53 is a standard library for sanitarying HTML on the client. If the application uses DOMPurify.sanitize(userInput) before inserting in DOM - it is believed that XSS is not possible. In the blacks, it says that everything is safe. CVE-2024-45801 proves the opposite.
The vulnerability was evaluated by CVSS 7.3 (HIGH) with CVSS vector:3.1/AV/A/AC:L/PR:N/UI:N/C/C:L/I:L/A:L/A:L. DOMPurify versions up to 2.5.4 and up to 3.1.3. Two CWEs describe the essence of the problem: CWE-1333 (Inefficient Regular Expression Complexity) - ineffective verification of the depth of HTML nesting, and CWE-1321 (Improperly Controlled Modification of Object Prototype Attributes) - insufficient protection from Prototype Pollution.
The attack works in two ways. The first is a specially designed HTML with deep nesting, which bypasses depth-checker DOMPurify. The second is Prototype Pollution: if the attacker can contaminate the prototype Object (through another vulnerability in the application or through gadget), it manipulates behavior Number.isNaNOn which the depth check depends. After that, any malicious HTML goes through sanitize() Like a knife through the oil.
The patch turned out to be elegant - in the code DOMPurify wrapped Number.isNaN the function unapply, which untied the method from the prototype and makes it immune to Prototype Pollution. According to GitHub Security Advisory (GHSA), in addition to CVE-2024-45801, DOMPurify has a vulnerability story: GHSA-39q2-94rc-95cp FORBID_TAGS through ADD_TAGS) and GHSA-636q7-h895-m982 (XSS in earlier versions, CVE-2020-26870).
For the penetester, the output is simple: if DOMPurify is detected on the target application - check the version. console.log(DOMPurify.version) in DevTools Console (if the library is available globally) or the analysis of the specific code on the characteristic lines will show whether a vulnerable version is used. If the application transmits HTML via DOMPurify < 3.3.3. - test bypass through deep nesting and Prototype Pollution.
Maxping XSS at MITRE ATT&CK: from introduction to impact
XSS vulnerability operation is not alert(1) for the screenshot. Each stage of attack through cross-site execution of scripts fits into specific MITRE ATT&CK techniques, and understanding this mapping is critical for writing and assessing the impact.
Initial Access Exploit Public-Faction Application (T1190) The attacker exploits the XSS vulnerability web application for the initial application of the code in the context of the victim’s browser. For XSS, you need to deliver URL to the victim, for the store - it is enough to wait for the visit to the infected page.
Execution - JavaScript (T1059.007). The implemented script is executed by the JavaScript browser engine. The attacker is limited to the browser sandbox, but inside it can perform arbitrary code.
Credential Access - Steal Web Session Cookies (T1539). Classic session hijacking via XSS: script reads document.cookie and sends it to the attacker's server. Works only in the absence of a flag HttpOnly in session cookies. In parallel - Web Portal Capture (T1056.003): creating a fake form of the login over a legitimate application for intercepting accounts.
Collection - Browser Session Hijacking (T1185). The attacker uses a stolen session to perform actions on behalf of the victim: changing email, password, data output.
Defense Evasion - Command Obfuscation (T1027.010). Multi-stage delivery payload, base64-coding, download lines through fetch - bypass WAF and CSP.
Impact External Defacement (T1491.002) and Transmitted Data Manipulation (T1565.002). Substitution of page content for phishing or defacea, modification of these forms before being sent to the server.
Such a mapping in a report turns “XSS into a search box” into a chain of attack with a specific impact, which seriously affects the assessment of the XSS patient pentest. Without a mapping - "medium, well, XSS". With the mapping - "high, because T1539 → T1185 → Complete Capture of the Administrator's Account".
Content Security Policy bypass: what works in practice
CSP is the last line of defense that blocks the execution of online scripts even with a successful HTML injection. Strict Politics script-src 'self' Prohibits online handlers and third-party scripts. But in practice, CSP is often leaky.
The most frequent configuration errors: Availability 'unsafe-inline' (completely kills CSP vs XSS), use 'unsafe-eval' (allows eval() in peyloads), whitelist-approach with CDN domains. The last one is my favorite find: if cdnjs.cloudflare.com in a whitelist, the attacker downloads from there a vulnerable Angular library and executes arbitrary code. The developer thought he allowed “DCN only” and actually opened the door.
Approach with nonce (script-src 'nonce-R4nd0m') more reliable, but requires a unique nonce for each request. If nonce is static or predictable, the CSP bypass is trivial. Also, if there is a JSONP endpoint on a whitelisted domain on the page, the attacker can use it as gadget to execute code: <script src="https://whitelisted.com/jsonp?callback=alert"></script>.
Procedure for the audit of the CSP: copy the title Content-Security-Policy from the answer, drive through Google CSP Evaluator (csp-evaluator.withgoogle.com), identify weak directives and build a vector of circumvention for a specific policy. It takes five minutes, and the result is unexpected - on one project, the CSP Evaluator showed three high-serity problems in politics, which the customer considered "strict".
Three types of cross-site scripting through the prism of HTTP requests
Russian-language materials usually describe three types of XSS in abstract: “reflected returns in response”, “storey stored on the server”, “DOM-based works in the browser”. Formally true, but it is useless for the pentester - we need a discussion at the level of specific requests and points of implementation.
Reflected XSS attack - from GET-parameter to execution
Reflected XSS occurs when the value from the request (GET parameter, POST, HTTP header) gets into the server’s response without processing. The key word is “context of insertion”. The same input behaves completely differently depending on where it lands in HTML.
Let's say there is a search on the target site: GET /search?q=test. The server returns the line Results for your search:: test within the tag <h2>. The first step is to throw through Burp Repeater value like aaa"'<>{} and see what symbols are passing. If you see unedited <> - it's an HTML context, and the classic vector will work through the tag.
But more often it is different. The value gets inside the attribute: <input value="sissejuhatus">. Here <script> useless - you must first get out of the attribute through ">, and then implement your tag. Peyload "><img src=x onerror=alert(1)> closes the attribute, closes the tag and creates a new element with the event handler.
The third context is JavaScript. If the input is included in the structure var x = 'ВВОД';, you need to get out of the line: ';alert(1)//. Each context requires its own pelohad, and that is why blind phases without analyzing the answer is a waste of time.
According to YesWeHack, XSS reflects most often found in search forms, login forms and URLs. When using reflect XSS, the attacker forms a URL with a paced and delivers it to the victim through phishing. If the session for a book without a flag HttpOnly, simple fetch c document.cookie in the parameter sends the session token to the attacker's server. Direct path to the hijacking of the session via XSS (Steal Web Session Cookies, T1539 on MITRE ATT&CK).
Stored XSS Operating - Persistent Vector
Stored XSS is the most dangerous type because the peyload is stored on the server and works at each visitor to the infected page. The introduction of a malicious script goes through the fields that fall into the database: comments, user profiles, support labels, file names.
In practice, stored XSS exploits look like this. The attacker finds a field that takes input and displays it to other users. Throws there a tag with the processor of the event - for example, <img src=x onerror=alert(document.domain)>. If when you view a page with another user, the browser draws this tag and tries to download a non-existent image - it works onerror, and JavaScript injection is executed in the context of the victim session.
A separate subspecies is blind XSS. Peyloade is sent through the feedback form or ticket and works only in the admin, when the employee opens the application. The attacker does not see the result immediately, so he uses an external callback server (Burp Collaborator or its own) to confirm the trigger. Blind XSS is especially valuable for bug bounty - it affects privileged users and is often rated as high or critical. I am on the same project through blind XSS in the support label pulled the session of the administrator - the application hung in line for three days, and all this time the peload waited patiently.
DOM-based XSS is different in that the server response can be completely secure – the vulnerability lies in the client JavaScript, which takes data from a controlled source (source) and inserts them into a dangerous receiver (sink).
Typical source: location.hash, location.search, document.referrer, window.name, postMessage. Typical: sinks innerHTML, outerHTML, document.write, eval(), setTimeout() with string argument, jQuery.html().
The DOM-based XSS search method is reduced to two steps. The first is to find in the sink code. In DevTools on the Sources tab innerHTML, eval and other dangerous methods. The second is to see where the data comes from. If the chain from source to sink does not contain sanitation, it is DOM-based XSS.
A specific cross-site scripting example: the script takes the value from window.location.hash through decodeURIComponent, then insert it through innerHTML in the item on the page. Transition to the URL https://target.com/#<img src=x onerror=alert(1)> leads to the execution of the code without any interaction with the server. WAF, server validation - are powerless, because the peyloade does not leave the browser.
In React applications, a similar problem arises when using dangerouslySetInnerHTML with user input. In Angular - when bypassing DomSantiizer through bypassSecurityTrustHtml. Modern frameworks protect against XSS by default, but the developers deliberately disable the protection for the sake of “flexibility” (and then wonder where they came from).
Search XSS Vulnerabilities: Step-by-step method of pentest
Adjustments to the environment: Burp Suite Community/Pro (actual version), browser with customized proxy (FoxyProxy or system proxy on 127.0.0.1:8080), access to the target application with a test resolution. For DOM-analysis - Chrome DevTools. For automated phases - wordlist with XSS-dipal (recommended SecLists xss-payload-list.txt - to start with enough eyes).
The first step is to the attack surface of the attack. I proxy all traffic through Burp, pass through the appendix, fill out all forms, click all links. In Target > Site map I see a complete parameter map. Each parameter that is reflected in the response is a potential point for XSS. Each parameter that is saved and displayed later is a candidate for the XSS store.
The second step is manual review of the context. For each reflected parameter, I send a test line with special symbols to Repeator: '"<>(){}. I see the answer - that is shielded, what is left. This determines the attack vector. If <> screened, but '" no, and the input falls into the attribute - you can get out of the attribute and use the event handler without creating a new tag: " onfocus=alert(1) autofocus=". Do not rush to phase immediately - first understand the context.
Step three is the fan-the-facing event-handlers. If the standard onerror, onclick, onload blocked WAF, sorting less well-known handlers. To do this, I use PortSwigger XSS Cheat Sheet (an actual version of 2026) - there are dozens of event-landlers by one that require and do not require user interaction. onanimationend, ontransitionend, onscrollend, onbeforematch - all these are real ways to trigger JavaScript without a click.
Step four: DOM check. I open DevTools > Sources, looking for keywords innerHTML, document.write, eval, $.html(. For each found, I monitor the data source. If the source is controlled (hash, query, postMessage) - testing the introduction through it.
Step five - check of the store XSS. Insert peyloade in each field that is saved: username, biography, comment, file name when downloading. Then I check how this input is displayed - in the personal account, in the admin, in email notifications. Blind XSS checks via callback on Burp Collaborator: I implement <img src=https://COLLABORATOR_ID.burpcollaborator.net> and awaiting HTTP circulation.
In addition to the exotic event-handlers, according to the PortSwigger XSS Cheat Sheet, the following approaches to bypass work:
• CSS-animation as a trigger: onanimationstart, ontransitionendand their webkit versions are triggered without user interaction in the presence of CSS rules with @keyframes
• Consuming tags: unclosed tags that “absorb” part of the page and change the context of parsing
• Encoding: HTML-entities (onerror), Unicode-escaped sequence in JavaScript, double URL-encode to bypass server decoders
• Framework-specific: in Vue.js - {{constructor.constructor('alert(1)')()}}for template injection; in AngularJS - sandbox escape through $eval
CVE-2024-45801 - when DOMPurify stops defending
DOMPurify from Cure53 is a standard library for sanitarying HTML on the client. If the application uses DOMPurify.sanitize(userInput) before inserting in DOM - it is believed that XSS is not possible. In the blacks, it says that everything is safe. CVE-2024-45801 proves the opposite.
The vulnerability was evaluated by CVSS 7.3 (HIGH) with CVSS vector:3.1/AV/A/AC:L/PR:N/UI:N/C/C:L/I:L/A:L/A:L. DOMPurify versions up to 2.5.4 and up to 3.1.3. Two CWEs describe the essence of the problem: CWE-1333 (Inefficient Regular Expression Complexity) - ineffective verification of the depth of HTML nesting, and CWE-1321 (Improperly Controlled Modification of Object Prototype Attributes) - insufficient protection from Prototype Pollution.
The attack works in two ways. The first is a specially designed HTML with deep nesting, which bypasses depth-checker DOMPurify. The second is Prototype Pollution: if the attacker can contaminate the prototype Object (through another vulnerability in the application or through gadget), it manipulates behavior Number.isNaNOn which the depth check depends. After that, any malicious HTML goes through sanitize() Like a knife through the oil.
The patch turned out to be elegant - in the code DOMPurify wrapped Number.isNaN the function unapply, which untied the method from the prototype and makes it immune to Prototype Pollution. According to GitHub Security Advisory (GHSA), in addition to CVE-2024-45801, DOMPurify has a vulnerability story: GHSA-39q2-94rc-95cp FORBID_TAGS through ADD_TAGS) and GHSA-636q7-h895-m982 (XSS in earlier versions, CVE-2020-26870).
For the penetester, the output is simple: if DOMPurify is detected on the target application - check the version. console.log(DOMPurify.version) in DevTools Console (if the library is available globally) or the analysis of the specific code on the characteristic lines will show whether a vulnerable version is used. If the application transmits HTML via DOMPurify < 3.3.3. - test bypass through deep nesting and Prototype Pollution.
JavaScript:
// Check the DOMPurify version in the browser console
console.log(DOMPurify.version);
// If < 2.5.4 or < 3.1.3, it is vulnerable to CVE-2024-45801
// Concept of the Prototype Pollution gadget:
Object.prototype.__proto__ = { isNaN: () => true };
// After prototype pollution, DOMPurify.sanitize() passes through malicious HTML
Maxping XSS at MITRE ATT&CK: from introduction to impact
XSS vulnerability operation is not alert(1) for the screenshot. Each stage of attack through cross-site execution of scripts fits into specific MITRE ATT&CK techniques, and understanding this mapping is critical for writing and assessing the impact.
Initial Access Exploit Public-Faction Application (T1190) The attacker exploits the XSS vulnerability web application for the initial application of the code in the context of the victim’s browser. For XSS, you need to deliver URL to the victim, for the store - it is enough to wait for the visit to the infected page.
Execution - JavaScript (T1059.007). The implemented script is executed by the JavaScript browser engine. The attacker is limited to the browser sandbox, but inside it can perform arbitrary code.
Credential Access - Steal Web Session Cookies (T1539). Classic session hijacking via XSS: script reads document.cookie and sends it to the attacker's server. Works only in the absence of a flag HttpOnly in session cookies. In parallel - Web Portal Capture (T1056.003): creating a fake form of the login over a legitimate application for intercepting accounts.
Collection - Browser Session Hijacking (T1185). The attacker uses a stolen session to perform actions on behalf of the victim: changing email, password, data output.
Defense Evasion - Command Obfuscation (T1027.010). Multi-stage delivery payload, base64-coding, download lines through fetch - bypass WAF and CSP.
Impact External Defacement (T1491.002) and Transmitted Data Manipulation (T1565.002). Substitution of page content for phishing or defacea, modification of these forms before being sent to the server.
Such a mapping in a report turns “XSS into a search box” into a chain of attack with a specific impact, which seriously affects the assessment of the XSS patient pentest. Without a mapping - "medium, well, XSS". With the mapping - "high, because T1539 → T1185 → Complete Capture of the Administrator's Account".
Content Security Policy bypass: what works in practice
CSP is the last line of defense that blocks the execution of online scripts even with a successful HTML injection. Strict Politics script-src 'self' Prohibits online handlers and third-party scripts. But in practice, CSP is often leaky.
The most frequent configuration errors: Availability 'unsafe-inline' (completely kills CSP vs XSS), use 'unsafe-eval' (allows eval() in peyloads), whitelist-approach with CDN domains. The last one is my favorite find: if cdnjs.cloudflare.com in a whitelist, the attacker downloads from there a vulnerable Angular library and executes arbitrary code. The developer thought he allowed “DCN only” and actually opened the door.
Approach with nonce (script-src 'nonce-R4nd0m') more reliable, but requires a unique nonce for each request. If nonce is static or predictable, the CSP bypass is trivial. Also, if there is a JSONP endpoint on a whitelisted domain on the page, the attacker can use it as gadget to execute code: <script src="https://whitelisted.com/jsonp?callback=alert"></script>.
Procedure for the audit of the CSP: copy the title Content-Security-Policy from the answer, drive through Google CSP Evaluator (csp-evaluator.withgoogle.com), identify weak directives and build a vector of circumvention for a specific policy. It takes five minutes, and the result is unexpected - on one project, the CSP Evaluator showed three high-serity problems in politics, which the customer considered "strict".