CVE-2022-21122: Improper Control of Generation of Code ('Code Injection') (CWE-94)
value: The package metacalc before 0.0.2 are vulnerable to Arbitrary Code Execution when it exposes JavaScript's Math class to the v8 context. As the Math class is exposed to user-land, it can be used to get access to JavaScript's Function constructor.
CWE description: The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
CVSS v3 base score: 9.8
Published: 2022-06-08T09:15Z
Repository: https://github.com/metarhia/metacalc
Language: JavaScript
Files changed: CHANGELOG.md, lib/sheet.js, test/unit.js
Commit date: 2022-05-17 15:53:28 +0300
Commit message: Prevent arbitrary js code execution

PR-URL: https://github.com/metarhia/metacalc/pull/16

Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0f58e29..cc44b0c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,11 @@
 # Changelog
 
 ## [Unreleased][unreleased]
 
+- Prevent arbitrary js code execution
+
 ## [0.0.1][] - 2022-05-13
 
 - First simple implementation
 - Implement Math namespace
 
diff --git a/lib/sheet.js b/lib/sheet.js
index 8f9cfdf..105ce96 100644
--- a/lib/sheet.js
+++ b/lib/sheet.js
@@ -1,11 +1,23 @@
 'use strict';
 
 const metavm = require('metavm');
 
+const wrap = (target) =>
+  new Proxy(target, {
+    get: (target, prop) => {
+      if (prop === 'constructor') return null;
+      const value = target[prop];
+      if (typeof value === 'number') return value;
+      return wrap(value);
+    },
+  });
+
+const math = wrap(Math);
+
 const getValue = (target, prop) => {
-  if (prop === 'Math') return Math;
+  if (prop === 'Math') return math;
   const { expressions, data } = target;
   if (!expressions.has(prop)) return data.get(prop);
   const expression = expressions.get(prop);
   return expression();
 };
diff --git a/test/unit.js b/test/unit.js
index fb0fab4..af2c47e 100644
--- a/test/unit.js
+++ b/test/unit.js
@@ -46,5 +46,21 @@ metatests.test('JavaScript Math', async (test) => {
   test.strictSame(sheet.values['G1'], Math.sin(100));
   test.strictSame(sheet.values['H1'], Math.sqrt(100));
   test.strictSame(sheet.values['I1'], Math.sin(Math.sqrt(Math.pow(100, -2))));
   test.end();
 });
+
+metatests.test('Prevent arbitrary js code execution', async (test) => {
+  const sheet = new Sheet();
+  sheet.cells['A1'] =
+    '=Math.constructor.constructor("console.log(\\"Hello, World!\\")")();';
+  try {
+    const res = sheet.values['A1'];
+    test.strictSame(res, undefined);
+  } catch (error) {
+    test.strictSame(
+      error.message,
+      `Cannot read property '${'constructor'}' of null`
+    );
+  }
+  test.end();
+});

Vulnerable code:   if (prop === 'Math') return Math;
Fixed code: - Prevent arbitrary js code execution

const wrap = (target) =>
  new Proxy(target, {
    get: (target, prop) => {
      if (prop === 'constructor') return null;
      const value = target[prop];
      if (typeof value === 'number') return value;
      return wrap(value);
    },
  });

const math = wrap(Math);

  if (prop === 'Math') return math;

metatests.test('Prevent arbitrary js code execution', async (test) => {
  const sheet = new Sheet();
  sheet.cells['A1'] =
    '=Math.constructor.constructor("console.log(\\"Hello, World!\\")")();';
  try {
    const res = sheet.values['A1'];
    test.strictSame(res, undefined);
  } catch (error) {
    test.strictSame(
      error.message,
      `Cannot read property '${'constructor'}' of null`
    );
  }
  test.end();
});