CVE-2023-26487 (JavaScript, published 2023-03-04T00:15Z)
Description: value: Vega is a visualization grammar, a declarative format for creating, saving, and sharing interactive visualization designs.`lassoAppend' function accepts 3 arguments and internally invokes `push` function on the 1st argument specifying array consisting of 2nd and 3rd arguments as `push` call argument. The type of the 1st argument is supposed to be an array, but it's not enforced. This makes it possible to specify any object with a `push` function as the 1st argument, `push` function can be set to any function that can be access via `event.view` (no all such functions can be exploited due to invalid context or signature, but some can, e.g. `console.log`). The issue is that`lassoAppend` doesn't enforce proper types of its arguments. This issue opens various XSS vectors, but exact impact and severity depends on the environment (e.g. Core JS `setImmediate` polyfill basically allows `eval`-like functionality). This issue was patched in 5.23.0.

Severity: nan (CVSS v3 base score: 6.1)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/vega/vega
Files changed:
packages/vega-functions/src/functions/lasso.js
Commit message: fix: lassoAppend XSS w/typecheck + array spread instead of push


Vulnerable code:
import { Bounds } from 'vega-scenegraph';
 * 
    const last = lasso[lasso.length - 1];
    // Add point to lasso if distance to last point exceed minDist or its the first point
    if (last === undefined || Math.sqrt(((last[0] - x) ** 2) + ((last[1] - y) ** 2)) > minDist) {
        lasso.push([x, y]);

        return [...lasso];
    }

    return lasso;
 * 
    return (lasso ?? []).reduce((svg, [x, y], i) => {
        return svg += i == 0
            ? `M ${x},${y} `
            : i === lasso.length - 1
                ? ' Z'
                : `L ${x},${y} `;
    }, '');
 * 
 * 
    const { x, y, mark } = unit;

    const bb = new Bounds().set(
        Number.MAX_SAFE_INTEGER,
        Number.MAX_SAFE_INTEGER,
        Number.MIN_SAFE_INTEGER,
        Number.MIN_SAFE_INTEGER
    );

    // Get bounding box around lasso
    for (const [px, py] of pixelLasso) {
        if (px < bb.x1) bb.x1 = px;
        if (px > bb.x2) bb.x2 = px;
        if (py < bb.y1) bb.y1 = py;
        if (py > bb.y2) bb.y2 = py;
    }

    // Translate bb against unit coordinates
    bb.translate(x, y);

    const intersection = intersect([[bb.x1, bb.y1], [bb.x2, bb.y2]],
        markname,
        mark);

    // Check every point against the lasso
    return intersection.filter(tuple => pointInPolygon(tuple.x, tuple.y, pixelLasso));
 * 
 * 
    let intersections = 0;
    for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
        const [prevX, prevY] = polygon[j];
        const [x, y] = polygon[i];
        // count intersections
        if (((y > testy) != (prevY > testy)) && (testx < (prevX - x) * (testy - y) / (prevY - y) + x)) {
            intersections++;
        }
    // point is in polygon if intersection count is odd
    return intersections & 1;

Fixed code:
import {Bounds} from 'vega-scenegraph';
import {array} from 'vega-util';
 *
  lasso = array(lasso);
  const last = lasso[lasso.length - 1];
  // Add point to lasso if its the first point or distance to last point exceed minDist
  return (last === undefined || Math.sqrt(((last[0] - x) ** 2) + ((last[1] - y) ** 2)) > minDist)
  ? [...lasso, [x, y]]
  : lasso;
 *
  return array(lasso).reduce((svg, [x, y], i) => {
  return svg += i == 0
    ? `M ${x},${y} `
    : i === lasso.length - 1
    ? ' Z'
    : `L ${x},${y} `;
  }, '');
 *
 *
  const { x, y, mark } = unit;

  const bb = new Bounds().set(
    Number.MAX_SAFE_INTEGER,
    Number.MAX_SAFE_INTEGER,
    Number.MIN_SAFE_INTEGER,
    Number.MIN_SAFE_INTEGER
  );

  // Get bounding box around lasso
  for (const [px, py] of pixelLasso) {
    if (px < bb.x1) bb.x1 = px;
    if (px > bb.x2) bb.x2 = px;
    if (py < bb.y1) bb.y1 = py;
    if (py > bb.y2) bb.y2 = py;
  }

  // Translate bb against unit coordinates
  bb.translate(x, y);

  const intersection = intersect([[bb.x1, bb.y1], [bb.x2, bb.y2]],
    markname,
    mark);

  // Check every point against the lasso
  return intersection.filter(tuple => pointInPolygon(tuple.x, tuple.y, pixelLasso));
 *
 *
  let intersections = 0;
  for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
    const [prevX, prevY] = polygon[j];
    const [x, y] = polygon[i];
    // count intersections
    if (((y > testy) != (prevY > testy)) && (testx < (prevX - x) * (testy - y) / (prevY - y) + x)) {
      intersections++;
  }
  // point is in polygon if intersection count is odd
  return intersections & 1;