Manual Testing for DOM-Based Cross-Site Scripting (XSS)
In this installment of the Learn Pentesting series, we’ll explore the nuances of DOM-based Cross-Site Scripting (XSS) and detail a manual testing methodology that penetration testers can apply to uncover these vulnerabilities. This guide is designed to be a technical deep-dive, complete with code examples and step-by-step instructions to help you understand, identify, and exploit DOM-based XSS during web-application assessments.
Introduction to DOM-Based XSS
DOM-based XSS is a type of cross-site scripting vulnerability where the malicious payload is executed as a result of modifying the Document Object Model (DOM) in the victim’s browser, rather than through server-side responses. Unlike reflected or stored XSS, the vulnerability exists entirely in the client-side code, making detection and exploitation a challenge during penetration testing.
In a typical scenario, the JavaScript code running in the browser reads data from sources such as document.URL
, location.hash
, or localStorage
, and then dynamically writes that data to the page without proper sanitization. This unsanitized data might include attacker-controlled input that can lead to code execution.
Understanding the DOM Environment
Before diving into testing, it’s essential to understand the two main components that define DOM-based XSS vulnerabilities:
Sources: Locations in the DOM where data is read from. Common sources include:
window.location
document.URL
document.location
location.hash
localStorage
andsessionStorage
Sinks: Functions or properties that use or output the data. Examples include:
innerHTML
document.write()
eval()
setTimeout()
Function()
constructor
A vulnerability occurs when data flows from a source to a sink without proper validation or sanitization, allowing an attacker to inject and execute arbitrary JavaScript.
Identifying Sources and Sinks
Common Sources
- URL Components: The query string, hash (
#
), and even the pathname can sometimes be used. - Client-Side Storage: Data retrieved from
localStorage
orsessionStorage
that might be under user control. - User Inputs: Data gathered from form fields or other DOM elements.
Common Sinks
- HTML Injection Points: Using properties like
innerHTML
or methods likedocument.write()
. - Dynamic Code Execution: Functions such as
eval()
,setTimeout()
(with string arguments), or theFunction()
constructor.
By performing a static review of the JavaScript code or using browser developer tools, you can often identify these sources and sinks. Look for assignments where data from a source is passed directly to a sink.
Manual Testing Approach
Step 1: Reconnaissance and Environment Analysis
- Crawl the Application: Begin by mapping out the client-side functionality. Identify pages or components that handle user-controlled data.
- Review JavaScript Files: Check for functions that manipulate the DOM, especially those that take input from the URL or client-side storage.
Step 2: Source and Sink Identification
- Search for Data Flow: Use browser developer tools (like Chrome DevTools) to search for keywords such as
location
,hash
,innerHTML
, oreval
in the JavaScript source. - Static Analysis: Evaluate whether the code sanitizes inputs before passing them to dangerous sinks.
Step 3: Crafting the Payload
When crafting payloads, consider testing with simple scripts such as:
<script>alert('XSS')</script>
However, because many modern applications filter out <script>
tags or similar constructs, you might need to use alternative payloads that exploit the specific context. For instance, if the vulnerable code writes data into an HTML attribute, you could try:
" onmouseover="alert('XSS')"
Step 4: Observing Behavior and Verification
Manipulate URL Fragments: A common testing technique is to modify the URL hash and observe the resulting changes. For example, if a page uses
location.hash
directly, append a payload to the URL:http://example.com/vulnerable-page#<img src=x onerror=alert('XSS')>
Use Developer Tools: Monitor the DOM changes in real time. Set breakpoints in JavaScript execution to see how data flows from the source to the sink.
Iterate on Payloads: Adjust your payloads based on observed behavior. If the payload is sanitized, try different encoding or obfuscation methods until you see the injected code execute.
Example Walkthrough
Consider the following vulnerable snippet of HTML and JavaScript:
<!DOCTYPE html>
<html>
<head>
<title>DOM-based XSS Example</title>
</head>
<body>
<div id="content"></div>
<script>
// Vulnerable usage: directly setting innerHTML using the URL hash
var hash = window.location.hash.substring(1);
document.getElementById('content').innerHTML = hash;
</script>
</body>
</html>
Testing the Vulnerability
Step 1: Load the page normally.
Step 2: Modify the URL by appending a malicious payload to the hash. For example:
http://example.com/page.html#<img src=x onerror=alert('XSS')>
Step 3: Press Enter and observe the page. If the browser executes the payload (i.e., displays an alert dialog), then you have confirmed a DOM-based XSS vulnerability.
Enhancing the Test with Developer Tools
- Set Breakpoints: In your browser’s DevTools, set breakpoints in the script to monitor the value of
hash
and how it’s inserted into the DOM. - Inspect Element: Verify that the injected payload appears in the DOM without any sanitization.
- Experiment with Variations: Try using encoded payloads, different HTML tags, or even payloads that bypass filters implemented by the application.
Mitigation and Remediation Considerations
Once a DOM-based XSS vulnerability is identified, remediation should focus on safe coding practices:
- Sanitization: Use libraries like DOMPurify to cleanse user-controlled inputs before inserting them into the DOM.
- Safe APIs: Avoid dangerous functions such as
innerHTML
ordocument.write()
when possible. Instead, consider safer alternatives liketextContent
or proper DOM manipulation techniques. - CSP (Content Security Policy): Implementing a strong CSP can help mitigate the impact of XSS vulnerabilities by restricting the sources from which scripts can be loaded.
For developers, a deep understanding of how client-side data flows can reduce the risk of such vulnerabilities. For testers, ensuring that each potential source of user input is scrutinized for proper handling is crucial.
Conclusion and Next Steps
Manual testing for DOM-based XSS requires a detailed understanding of how JavaScript manipulates the DOM and how untrusted data can be exploited. In this guide, we walked through the process—from identifying sources and sinks, crafting payloads, to validating the vulnerability using practical examples.
For further exploration:
- Review additional client-side vulnerabilities.
- Experiment with different payloads and bypass techniques.
- Stay updated with the latest secure coding practices and testing tools.
The insights provided here are intended as a reference for penetration testers and security researchers. As part of our Learn Pentesting series, we aim to equip you with the technical knowledge to confidently assess and secure web applications.
Happy testing!