CVE-2023-26487: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
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.

CWE description: 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.
CVSS v3 base score: 6.1
Published: 2023-03-04T00:15Z
Repository: https://github.com/vega/vega
Language: JavaScript
Files changed: packages/vega-functions/src/functions/lasso.js
Commit date: 2023-02-28 12:07:00 -0800
Commit message: fix: lassoAppend XSS w/typecheck + array spread instead of push

Diff: diff --git a/packages/vega-functions/src/functions/lasso.js b/packages/vega-functions/src/functions/lasso.js
index 694e722e..de6135a2 100644
--- a/packages/vega-functions/src/functions/lasso.js
+++ b/packages/vega-functions/src/functions/lasso.js
@@ -1,110 +1,108 @@
 import intersect from './intersect';
-import { Bounds } from 'vega-scenegraph';
+import {Bounds} from 'vega-scenegraph';
+import {array} from 'vega-util';
 
 /**
  * Appends a new point to the lasso
- * 
+ *
  * @param {*} lasso the lasso in pixel space
  * @param {*} x the x coordinate in pixel space
  * @param {*} y the y coordinate in pixel space
  * @param {*} minDist the minimum distance, in pixels, that thenew point needs to be apart from the last point
  * @returns a new array containing the lasso with the new point
  */
 export function lassoAppend(lasso, x, y, minDist = 5) {
-    const last = lasso[lasso.length - 1];
+  lasso = array(lasso);
+  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;
+  // 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;
 }
 
 
 /**
  * Generates a svg path command which draws a lasso
- * 
+ *
  * @param {*} lasso the lasso in pixel space in the form [[x,y], [x,y], ...]
  * @returns the svg path command that draws the lasso
  */
 export function lassoPath(lasso) {
-    return (lasso ?? []).reduce((svg, [x, y], i) => {
-        return svg += i == 0
-            ? `M ${x},${y} `
-            : i === lasso.length - 1
-                ? ' Z'
-                : `L ${x},${y} `;
-    }, '');
+  return array(lasso).reduce((svg, [x, y], i) => {
+  return svg += i == 0
+    ? `M ${x},${y} `
+    : i === lasso.length - 1
+    ? ' Z'
+    : `L ${x},${y} `;
+  }, '');
 }
 
 
 
 /**
  * Inverts the lasso from pixel space to an array of vega scenegraph tuples
- * 
+ *
  * @param {*} data the dataset
  * @param {*} pixelLasso the lasso in pixel space, [[x,y], [x,y], ...]
  * @param {*} unit the unit where the lasso is defined
- * 
+ *
  * @returns an array of vega scenegraph tuples
  */
 export function intersectLasso(markname, pixelLasso, unit) {
-    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));
+  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));
 }
 
 
 
 /**
  * Performs a test if a point is inside a polygon based on the idea from
  * https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html
- * 
+ *
  * This method will not need the same start/end point since it wraps around the edges of the array
- * 
+ *
  * @param {*} test a point to test against
  * @param {*} polygon a polygon in the form [[x,y], [x,y], ...]
  * @returns true if the point lies inside the polygon, false otherwise
  */
 function pointInPolygon(testx, testy, polygon) {
-    let intersections = 0;
+  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];
+  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++;
-        }
+    // 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;
+  // point is in polygon if intersection count is odd
+  return intersections & 1;
 }

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;