Chart of Doom: Hijacking Vega's .map() for DOM XSS
Jan 6, 2026·7 min read
Executive Summary (TL;DR)
Vega blindly trusts that an input is an Array before calling `.map()` on it. Attackers can pass a malicious object instead, hijacking the method call to trigger 'gadgets' like `CanvasHandler.prototype.on`. In environments where debug globals are exposed, this chains directly into `eval()`, turning your innocent bar chart into a remote shell.
A high-severity DOM-based Cross-Site Scripting (XSS) vulnerability in the Vega visualization library. By exploiting a type confusion flaw in the `vega-selections` package, attackers can perform method hijacking to execute arbitrary JavaScript code via crafted JSON specifications.
The Hook: When Data Becomes Code
Visualization libraries are supposed to be the safe haven of the web. You feed them JSON, they give you pretty bars, lines, and pies. It is declarative. It is passive. It is safe. Or at least, that is what we tell the frontend developers so they can sleep at night.
But Vega isn't just a charting library; it's a "visualization grammar." It supports expressions, signals, and event handling. It parses your JSON and builds a reactive runtime graph. And whenever you have a complex runtime interpreting user-supplied logic, you have a playground for security researchers with a dark sense of humor.
CVE-2025-65110 (or GHSA-829q-m3qg-ph8r for the GitHub connoisseurs) is a beautiful example of JavaScript doing exactly what you asked it to do, which is usually the problem. It is a DOM-based XSS that doesn't rely on injection into HTML strings, but rather on Method Hijacking. It turns the internal logic of the library against itself, using existing code execution paths (gadgets) to achieve arbitrary execution. If your application exposes Vega spec definitions to users, you aren't just letting them draw charts; you might be letting them draw your doom.
The Flaw: If It Quacks Like a Duck...
The vulnerability lives in vega-selections, specifically in a helper function called vlSelectionTuples. The job of this function is to iterate over a dataset and extract tuples for selection logic. To do this, it uses the standard Array method .map().
In a strictly typed language, passing a non-array to a function expecting an array would result in a compile-time error or a runtime exception. But this is JavaScript, the Wild West of programming languages. In JavaScript, method calls are looked up dynamically at runtime.
The developers wrote code that effectively says: "Take this input array and call array.map(function(d) {...})." They assumed array would always be, well, an Array. They didn't check.
Here is the logic flaw: If an attacker controls the input to vlSelectionTuples, they can pass a plain JavaScript object that has a property named map. When the code executes array.map(...), it doesn't call the native Array prototype's map. It calls whatever function the attacker attached to that property. This is classic Method Hijacking.
[!NOTE] This isn't just about crashing the app. By controlling the function that gets called, and crucially, controlling the object context (
this), an attacker can force the engine to jump to any function available in the scope.
The Code: The One-Liner That Killed Security
Let's look at the smoking gun in packages/vega-selections/src/selectionTuples.js. It is almost tragically simple.
The Vulnerable Code
// The code assumes 'array' is an Array.
// If 'array' is an object { map: maliciousFunction }, we have RCE.
function vlSelectionTuples(array, ...) {
// ...
return array.map(function(d) {
// logic processing the tuple
});
}The exploit relies on the fact that vlSelectionTuples is accessible via the Vega expression parser, which evaluates signals defined in the JSON spec. The attacker feeds a crafted object into this function via a signal update.
The Fix
The mitigation is exactly what you would expect: a sanity check. The patch forces a check to ensure the input is actually an array before attempting to map over it.
// The Fix: Trust, but Verify.
function vlSelectionTuples(array, ...) {
// ...
return Array.isArray(array)
? array.map(function(d) { ... })
: [];
}This simple predicate Array.isArray(array) kills the exploit chain dead. If you pass a malicious object now, it simply returns an empty array, and the method hijacking attempt fizzles out.
The Exploit: Building a ROP Chain in JavaScript
Finding the entry point (vlSelectionTuples) is only half the battle. To actually achieve code execution (XSS), we need a Gadget. A gadget is a piece of existing code that does something dangerous when we misuse it.
In the context of the Vega Editor (and many debug setups), a global object VEGA_DEBUG exposes internal Vega classes. The researcher identified a perfect gadget: VEGA_DEBUG.vega.CanvasHandler.prototype.on.
Why this function? Because it uses this heavily. When we hijack .map(), the map function is called with our malicious object as this. We can control every property accessed by the gadget.
Here is the gadget logic we are abusing:
// CanvasHandler.prototype.on
on(a, o) {
const u = this.eventName(a); // 1. We control this.eventName
const d = this._handlers; // 2. We control this._handlers
// 3. We control this._handlerIndex
if (this._handlerIndex(d[u], a, o) < 0) { ... }
}The Attack Chain
- Entry: Trigger
vlSelectionTuples(maliciousObject)via a signal (e.g., onmousemove). - Hijack: The code calls
maliciousObject.map(...). We have setmapto point toCanvasHandler.prototype.on. - Gadget Execution:
onexecutes withthisbound to our object. - Property Masquerading:
- We set
eventNametoconsole.log. It returnsundefined(harmless). - We set
_handlersto{ undefined: 'alert(1)' }. - We set
_handlerIndextowindow.eval.
- We set
- The Prestige: The gadget executes
this._handlerIndex(d[u]). Substituting our values, this becomeswindow.eval('alert(1)').
Boom. Arbitrary code execution.
The Impact: More Than Just Popups
Why should we panic about a charting library? Because of where these charts live. Vega is used in Business Intelligence (BI) dashboards, data science notebooks (like Jupyter/Lab via Altair), and internal analytics tools.
This vulnerability is strictly Client-Side, but the impact is high because these tools often have privileged access.
- Scenario A: The Dashboard Trap: An attacker shares a "fixed" JSON configuration for a KPI dashboard with an admin. The admin loads it. The script executes in the admin's session, potentially exfiltrating sensitive financial data or modifying user permissions.
- Scenario B: The Jupyter Pivot: In environments where Vega charts are rendered automatically (e.g., some notebook viewers), simply viewing a malicious dataset could trigger code execution within the context of the notebook server's domain.
The CVSS score is 8.1 (High) because while it requires user interaction (loading the chart), the complexity is low once the gadget chain is known, and the confidentiality/integrity impact is total within the scope of the DOM.
The Fix: Closing the Window
If you are using Vega, you have two jobs right now.
1. Update Your Dependencies
Check your package.json and yarn.lock / package-lock.json. You need to ensure vega-selections is updated.
- For Vega v5 users: Update to
vega-selections >= 5.6.3. - For Vega v6 users: Update to
vega-selections >= 6.1.2.
2. Remove Production Debuggers
The exploit chain described relies heavily on VEGA_DEBUG being available in the global scope to access the gadget (CanvasHandler). If you do not attach Vega internals to window, you significantly raise the bar for exploitation (though other standard gadgets might still exist in the standard library).
3. Content Security Policy (CSP)
This exploit ultimately relies on eval() (or a function constructor). A strong CSP is your safety net. Disallow unsafe-eval in your script-src directive. If you do that, even if the attacker hijacks the method, they can't compile their string payload into executable code.
Technical Appendix
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:NAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
vega-selections vega | < 5.6.3 | 5.6.3 |
vega-selections vega | >= 6.0.0, < 6.1.2 | 6.1.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 |
| Attack Vector | Network (DOM-based) |
| CVSS v3.1 | 8.1 (High) |
| Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N |
| Exploit Status | PoC Available |
| Patch Date | 2026-01-05 |
MITRE ATT&CK Mapping
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.