A Bug Bounty Tester’s Guide to Detecting XSS Vulnerabilities

Cross Site Scripting (XSS) vulnerabilities occur when web applications include untrusted data on a web page without proper sanitization and validation of user input, such as when a web page includes user-supplied data using a browser API that can create HTML or JavaScript. The XSS vulnerability is exploited when an attacker executes malicious scripts in the victim’s browser. Common XSS exploits aim to hijack user sessions, deface web sites, or redirect victims to malicious sites for chained exploitation.

A topic typically forgotten when discussing XSS is the Same-Origin Policy (SOP). With SOP, browsers trust all code coming from the origin site. XSS attacks exploit this policy by tricking the browser into believing malicious code came from a trusted site. XSS exploits are primarily defined in literature as either client- or server-side attacks. From there, they are further sub-categorized into Persistent-, Reflected-, and DOM-based XSS attacks. The first two are server-side attacks whereas the latter is a client-side attack.

Treat this post as a step-by-step guide on identifying XSS vulnerabilities rather than a tool-tutorial. I plan to make a post on XSS tools in a later post.


Not all XSS attacks are created equally; therefore, to understand this vulnerability, it is meaningful to break it down into its three main categories, discussed below.

Persistent XSS:

Stored XSS example
Imperva (2021) Persistent XSS

In a Persistent XSS attack, an attacker finds a susceptible site that is vulnerable to script injection. These injection points can be anywhere in which user input is accepted by the web application, such as comment boxes, form fields, sign-up forms, username fields, contact forms, etc. Attackers “inject” malicious JavaScript into these injection points by submitting malicious code to the server in a POST request. In turn, the malicious code is sent to the server and stored in the database. Forums or comment sections are popular targets of Persistent XSS attacks because previous user input stored within the database is served back to visitors when they visit the web page. In essence, the malicious script “persists” among the other valid data within the database, hence why the attack is also referred to as a Stored XSS attack.

Non-Persistent XSS:

Imperva (2021) Non-Persistent XSS

The Non-Persistent XSS attack also exploits a vulnerable injection point. The typical execution involves amending malicious JavaScript as a parameter query in the URL, which causes arbitrary code execution. The parameters in a GET request appear after the question mark (?) in a URL and these parameters within the URL are often (but not always) generated by form input on a Web page. Online tutorials usually demonstrate this by adding <script>alert(‘test’);</script> to the end of a URL, but XSS has evolved so rapidly that it is not that simple anymore. The Non-Persistent XSS also involves a dash of social engineering since the attack is activated by a link; therefore, the victim must be tricked into clicking the payload. The payload is usually delivered via a phishing email. If the script inside the URL evades the sanitization methods on the backend, then the incoming request will be immediately reflected off of the web application, hence the alternative name, a Reflected XSS attack. This is one of the major differences between a Persistent and a Non-Persistent XSS attack. In the Non-Persistent XSS attack, the attack happens immediately.

DOM-Based XSS:

Image result for dom-based xss
Makarem (2018). DOM-Based Cross Site Scripting (DOM-XSS)

Unlike the Persistent and Non-Persistent XSS attacks, which exploits a vulnerable website by having the server insert a malicious script, the DOM-based XSS attack exploits a vulnerability in the client’s web browser, making it a truly client-side attack. Additionally, with Persistent/Non-Persistent XSS attacks, the payload is injected into the application during server-side processing of requests where untrusted input is dynamically added to HTML. But for DOM-based XSS, the attack is injected directly into the application during runtime in the client.

With DOM-based XSS attacks, attackers manipulate or replace a specific parts of a web site’s existing code to execute their own malicious JavaScript. Documentation on DOM-based XSS frequently references Sources and Sinks. Sources are JavaScript object properties that contain controllable data, such as document.URL, document.documentURI, document.location, location.href, location.search, window.name, location.search, location.header, or document.referrer. XSS vulnerabilities can all be introduced through these sources.

On the other hand, Sinks are DOM objects or JavaScript methods that allow for code execution, which in turn update the DOM. Examples of Sinks include eval, setTimeout, setInterval, document.write, document.writeIn, innerHTML, outerHTML, document.body.innerHTML, or execScript. These sinks are where untrusted input at the Source get outputted on the page or executed by JavaScript within the page. Together, attackers test if input placed in a Source will extend to the Sink. Using JavaScript to reference specific nodes in the DOM, content can be accessed or modified on the web page. For example, the JavaScript method document.querySelector("p") retrieves the first HTML <p> element (or the first paragraph) in the DOM. Likewise, the JavaScript method document.getElementById(“Example”).innerHTML = “some random string” will retrieve all text content inside the “Example” ID of the DOM and overwrite the current HTML content within that ID. Now, imagine changing "some random string" to a malicious <script>. This is how the DOM-based XSS works.

DOM-based XSS attacks can be confusing, so here is a demonstration from Muscat (2014). The following HTML in the Figure below allows users to select a default language by using a query string.

If a user selected “German” as the default language, then the URL would like the following:

Assuming input is not sanitized, this could be a DOM-based XSS vulnerability because the source, document.location.href, sets the entire URL of the current page. When untrusted JavaScript is appended to the query string (after the “=” sign), it is reflected back into the page inside the <select> tag. In such as situation, an attacker could append his/her own JavaScript to the query:

The execution Sink in this example is document.write because it writes the new code to the current page. If a user visits this link, a JavaScript alert will open in the browser because the DOM is updated to include the attacker’s JavaScript.

Blind XSS (BXSS) – An Honorable Mention

The Blind XSS (BXSS) is a subcategory of the Persistent XSS attack whereby the attacker blindly targets web pages that store user input in a database, such as blogs, forums, comment boxes, contact forms, login forms, and message boards. Although the BXSS and the Persistent XSS attacks are fairly identical in terms of the end-goal and the stored payload, the distinction lies in the execution of the attack. In a BXSS attack, the attacker fires off payloads into vulnerable sites, but doing so manually is cumbersome. The process is instead automated using XSS security tools, such as XSS Hunter or the XSS Validator extension in Burp Suite. These tools automatically generate payloads and even correlates injection attempts.

The attacker does not expect to see any immediate response or sign that the attack worked; they are not even aware if the payload was stored in the same web application or even stored in the first place…hence the name, “blind” XSS. The BXSS sometimes targets site administrators because the malicious script is commonly either sanitized before being served to everyday site visitors or served by a different web application. Site admins/moderators who use management web sessions to load content for administrative tasks might find themselves executing the malicious code in the admin interface. For example, if attacker injects malicious code in a contact form and submits it to the server, the code will be stored in the database, but not likely in the same application. Thus, there is no immediate response, such as the typical JavaScript alert() box seen in other XSS testing procedures. The browser of a victim who visits the same contact page is not authorized to make a GET request for the attacker’s payload stored inside the database; however, a site admin who is reviewing the everyday contact submissions from site visitors may inadvertently execute the malicious script stored inside the attacker’s contact submission and voila, a Blind XSS attack occurs.


To test for XSS vulnerabilities, web developers or testers must determine every area that the web application accepts user input and identify how this input is processed. This includes not-so-obvious inputs, such as HTTP requests, POST data, hidden form field values, and pre-defined radio or selection values. Testers can accomplish this task by crafting their own harmless input (e.g., <script>alert('XSS');</script>) and submitting them as input to the server. Otherwise, they can use application fuzzers to generate random or predefined lists of known attack strings. A common tool used to accomplish this task is Burp Suite and its XSS-Validator extension, powered by PhantomJS.

However, testing for XSS vulnerabilities in modern web applications should not be as easy as simply submitting <script>alert(‘XSS’);</script> into input fields. The critical aspect of building modern web applications is that good web developers sanitize user input, which is accomplished using secure coding practices or Web Application Firewalls (WAFs). Since WAFs cannot recognize unknown attacks, the majority of XSS prevention relies on the developer’s sanitization techniques, such as returning errors, removing encoding, or replacing invalid input. This tends to be pretty successful if done properly; thus, simple XSS JavaScript alert() code injection like the example above will most likely fail. To bypass these filters, testers need to get creative with their payloads by manipulating them in a way that they bypass filters. This typically involves using malformed <img> tags, HTML entities, malformed <a> tags, encoding, iframes, and so on. OWASP has an XSS Filter Evasion Cheat Sheet that reveals a comprehensive list of fancy tricks. Likewise, PortSwigger has its own XSS Filter Evasion Cheat Sheet that accomplishes the same purpose, albeit, a little more comprehensively.

Testers must also inspect the source of the page. In FireFox, for example, this can be done by simply right-clicking anywhere on the web page and clicking “Inspect Element” (shortcut = F12). This is important because when testing for possible XSS vulnerabilities in the URL, testers can see where the payload is reflecting in the DOM and which characters in the script are being sanitized. For instance, if the web page has a “Search” feature that allows users to search content throughout the web site via keywords or tags, a script (e.g., /><script>alert(1)</script>) could be submitted through the search bar. After submission, testers should inspect the page code to see where this payload was reflected. If the payload fails, testers can check the code to see the reason why. For instance, if the script was modified from  "/><script>alert(1)</script> to “"/>alert(1)”, then the testers can identify HOW the script tags were sanitized. From there, the testers can resort to alternative evasion techniques.

Step 1 – Source Code Inspection:

Retire.js is a node module that has both Node and CLI components. It analyzes client-side JavaScript and Node modules for previously reported vulnerabilities. Testers can install it easily using $PATH: npm install -g retire. Reporting a bug that may have been discovered in a vendor’s software, but still requires addressing or patching in a company’s web application, often merits a reward. The easy-to-use CLI of retire makes it simple to write short, purpose-driven scripts in the Unix style.

It should be noted that the output of retire can be hard to read, but testers can use some of its available flags to rectify this. For example, the data can be outputted in json format using –outputformat json. The end product scans the client-side code of a web site and compiles a report in json format, which it can save to a json file and display to the tester. But to make sense of that json, testers can format it in a way that it pulls out the critical information (e.g., severity, description, and location) while leaving out the noise (e.g., dependency graphs). Testers can create a Python file that can be used for string manipulation and general data munging, to write a script that formats the json into a plain text report.

Fortunately, if set up is to laborious, FireFox includes a Retire.JS add-on that can be enabled in the browser. This option is much easie to set up and understand:

Step 2 – Automated Scanners:

It is not usually feasible to manually insert an XSS string into every injection point and determine if the payload executes or not. This takes too much time. Thus, leveraging a tool is the best approach; however, manual verification is still necessary if a vulnerability is discovered. Here are some tools that I use to locate XSS vulnerabilities:

Epsylon’s XSSer:

Epsylon’s XSSer is an automatic -framework- to detect, exploit and report XSS vulnerabilities in web-based applications. It provides several options bypass certain filters and various special techniques for code injection. XSSer also includes pre-installed (>1300 XSS) attacking vectors and can bypass-exploit code on several browsers/WAFs.

S0md3v’s XSStrike:

S0md3v’s XSStrike is a detection suite equipped with four hand written parsers, an intelligent payload generator, a powerful fuzzing engine, and an incredibly fast crawler. Instead of injecting payloads and checking it works like all the other tools do, XSStrike analyses the response with multiple parsers and then crafts payloads that are guaranteed to work by context analysis integrated with a fuzzing engine. Here are some examples of the payloads generated by XSStrike. Apart from that, XSStrike has crawling, fuzzing, parameter discovery, WAF detection capabilities as well. It also scans for DOM XSS vulnerabilities, which is a feature that sets it apart from most XSS tools.

PortSwigger’s XSS-Validator, Powered by PhantomJS:

PortSwigger’s XSS-Validator Extension is a burp intruder extender designed for automation and validation of XSS vulnerabilities. XSS Validator is designed to forward responses to the XSS detection server, which must run both externally and simultaneously to the extender. The XSS detection server is powered by Phantom.js and/or Slimer.js. Instructions on how to set this up in Burp can be found on PortSwigger’s Github.

Step 3 – XSS Filter Bypass:

No modern web application is without XSS filters; therefore, expect user input to be sanitized wherever the application accepts data. Be sure to utilize tools that can encode payloads for obfuscation. If the tester has an idea of what the filter is performing, then try bypassing these filters using encoding or modifying payloads to break through the filter. Tip: XSStrike can actually help identify what the filters are blocking:


Step 4 – Consider File Upload Vulnerabilities:

From OWASP: Do not forget about file upload vulnerabilities. If the web application allows file upload, it is important to check if it is possible to upload HTML content. For instance, if HTML or TXT files are allowed, XSS payload can be injected in the file uploaded. The pen-tester should also verify if the file upload allows setting arbitrary MIME types in Burp Suite. This design flaw can be exploited in browser MIME mishandling attacks. For instance, innocuous-looking files like JPG and GIF can contain an XSS payload that is executed when they are loaded by the browser. This is possible when the MIME type for an image such as image/gif can instead be set to text/html. In this case the file is treated by the client browser as HTML.

Always be on the lookout for URL parameters that might be reflected on the page because it’s possible to take control over these values. If URL parameters are rendered on the page, consider their context as well. URL parameters might present opportunities to get around filters that remove special characters. In Yaworski (2019), Mahmoud Jamal was using Google Images to find images for his Hackerone profile. While browsing, he noticed the image URL http://www.google.comimgres?imgurl=https://lh3.googleuser.com/… from Google. Noting the reference to imgurl, Jamal realized he could control the parameter’s value; it would likely be rendered on the page as a link. When hovering over the thumbnail image for his profile, Jamal confirmed that the <a> tag href attribute included the same URL. He tried changing the imgurl parameter to javascript:alert(1) and noticed that the href attribute also changed to the same value. Having JavaScript knowledge is essential for confirming more complex vulnerabilities.

Step 5 – Verification:

If a automated scanner finds an XSS vulnerability, be sure to manually verify it exists. Unfortunately, many automated scanners discover false positives instead of true positives.

More Bug Bounty Tips:

  1. Consider learning JavaScript. A bug bounty tester does not need to be an expert to understand how XSS attacks occur, but JavaScript is beneficial to understand in identifying XSS vulnerabilities and exploiting them during bug bounty engagements.
  2. Hackvector is great for obfuscating payloads.
  3. Ideally all HTML special characters will be replaced with HTML entities. The key HTML entities to identify are (>, <, &, ’, ”, \n, \r, \, \uxxxx).
  4. XSS vulnerabilities do not have to be intricate. When testing for XSS, be sure to view the page source and confirm whether the payloads are being rendered in HTML or JavaScript tags.
  5. When sites sanitize input by modifying it instead of encoding or escaping values, testers should continue testing the site’s server-side logic. Think about how a developer might have coded their solution and what assumptions they have made. For example, check whether the developer considered what happens if two src attributes are submitted or if spaces are replaced with slashes.


Borso, S. (2019). The Penetration Tester’s Guide to Web Applications. Artech House

Kim, P. (2015). The Hacker Playbook 2. Secure Planet, LLC

Kim, P. (2018). The Hacker Playbook 3. Secure Planet, LLC

Makarem, C. (2018). DOM-Based Cross Site Scripting (DOM-XSS). Medium. Retrieved from https://medium.com/iocscan/dom-based-cross-site-scripting-dom-xss-3396453364fd

Marshal, J. (2018). Hands-Om Bug Hunting for Penetration Testers. Packt Publishing

Muscat, I (2014). Finding the Source of a DOM-based XSS Vulnerability with Acunetix. Acutenix 2021. Retrieved from https://www.acunetix.com/blog/articles/finding-source-dom-based-xss-vulnerability-acunetix-wvs/

Velu, V. K. & Beggs, R. (2019). Mastering Kali Linux for Advanced Penetration Testing. Packt Publishing

  1. One major flaw I’ve noticed with XSS exploits (and I may be wrong on this since I’m still learning about them) is that if you want to inject code that steals cookie information and sends it back to you, you are in the process revealing your IP address to the server, and they can look at the source codes for the content you’ve posted and see the IP address listed in the code. I think it would be more prudent to have the malicious script send an email to a private address that doesn’t have any PII associated with it, then access the email account through a VPN or Tor and copy the information from the email into say Burp Intruder to perform an unauthorized login. What do you think of this?



    1. You’re correct that hackers need to worry about hiding their identity, but this post is about bug bounty testing. Testers have permission to test web applications and don’t need to worry about hiding their IP.



      1. Yeah, I’m just speculating that for people who are concerned about being anonymous, XSS exploits might not be the smartest idea, unless there’s some sort of proxy you can route the information through, and you can’t just route packets through Tor or something from the server end.


  2. I’m sharing this on my Twitter, ’cause I think it’s a really good tutorial.



Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: