CVE-2022-25759: Improper Control of Generation of Code ('Code Injection') (CWE-94)
value: The package convert-svg-core before 0.6.2 are vulnerable to Remote Code Injection via sending an SVG file containing the payload.
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-07-22T20:15Z
Repository: https://github.com/neocotic/convert-svg
Language: JSON
Files changed: package-lock.json, packages/convert-svg-core/package.json, packages/convert-svg-core/src/Converter.js, packages/convert-svg-test-helper/src/fixtures/input/issue-81.svg, packages/convert-svg-test-helper/src/tests.json
Commit date: 2022-05-29 16:40:52 +0100
Commit message: Strip onload attribute from SVG input

In order to prevent a remote code injection, the `onload` attribute needs to be removed from the SVG input as Puppeteer/Chromium will execute any code within it which could potentially be malicious.

Fixes #81

Diff: diff --git a/package-lock.json b/package-lock.json
index 78f854f..a566005 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,11 +6,11 @@
 		"": {
 			"name": "convert-svg",
 			"license": "MIT",
 			"dependencies": {
 				"chalk": "^4.1.2",
-				"cheerio": "^1.0.0-rc.10",
+				"cheerio": "^1.0.0-rc.11",
 				"commander": "^9.2.0",
 				"file-url": "^3.0.0",
 				"get-stdin": "^8.0.0",
 				"glob": "^8.0.1",
 				"lodash.clonedeep": "^4.5.0",
diff --git a/packages/convert-svg-core/package.json b/packages/convert-svg-core/package.json
index 02ef43b..a5fa8e9 100644
--- a/packages/convert-svg-core/package.json
+++ b/packages/convert-svg-core/package.json
@@ -34,11 +34,11 @@
     "url": "https://github.com/neocotic/convert-svg.git",
     "directory": "packages/convert-svg-core"
   },
   "dependencies": {
     "chalk": "^4.1.2",
-    "cheerio": "^1.0.0-rc.10",
+    "cheerio": "^1.0.0-rc.11",
     "commander": "^9.2.0",
     "file-url": "^3.0.0",
     "get-stdin": "^8.0.0",
     "glob": "^8.0.1",
     "lodash.omit": "^4.5.0",
diff --git a/packages/convert-svg-core/src/Converter.js b/packages/convert-svg-core/src/Converter.js
index 9d43043..12653ba 100644
--- a/packages/convert-svg-core/src/Converter.js
+++ b/packages/convert-svg-core/src/Converter.js
@@ -45,10 +45,11 @@ const _options = Symbol('options');
 const _page = Symbol('page');
 const _parseOptions = Symbol('parseOptions');
 const _provider = Symbol('provider');
 const _roundDimension = Symbol('roundDimension');
 const _roundDimensions = Symbol('roundDimensions');
+const _sanitize = Symbol('sanitize');
 const _setDimensions = Symbol('setDimensions');
 const _tempFile = Symbol('tempFile');
 const _validate = Symbol('validate');
 
 /**
@@ -112,13 +113,11 @@ class Converter {
   async convert(input, options) {
     this[_validate]();
 
     options = this[_parseOptions](options);
 
-    const output = await this[_convert](input, options);
-
-    return output;
+    return await this[_convert](input, options);
   }
 
   /**
    * Converts the SVG file at the specified path into another format using the <code>options</code> provided and writes
    * it to the output file.
@@ -189,15 +188,17 @@ class Converter {
 
   async [_convert](input, options) {
     input = Buffer.isBuffer(input) ? input.toString('utf8') : input;
 
     const { provider } = this;
-    const svg = cheerio.default.html(cheerio.load(input, null, false)('svg'));
+    const svg = cheerio.default.html(this[_sanitize](cheerio.load(input, null, false)('svg')));
+
+    if (!svg) {
+      throw new Error('SVG element not found in input. Check the SVG input');
+    }
 
-    let html = '';
-    if (svg) {
-      html += `<!DOCTYPE html>
+    const html = `<!DOCTYPE html>
 <html>
 <head>
 <base href="${options.baseUrl}">
 <meta charset="utf-8">
 <style>
@@ -205,13 +206,10 @@ class Converter {
 html { background-color: ${provider.getBackgroundColor(options)}; }
 </style>
 </head>
 <body>${svg}</body>
 </html>`;
-    } else {
-      throw new Error('SVG element not found in input. Check the SVG input');
-    }
 
     const page = await this[_getPage](html);
 
     await this[_setDimensions](page, options);
 
@@ -224,16 +222,14 @@ html { background-color: ${provider.getBackgroundColor(options)}; }
       await this[_setDimensions](page, dimensions);
     }
 
     await page.setViewport(dimensions);
 
-    const output = await page.screenshot(Object.assign({
+    return await page.screenshot(Object.assign({
       type: provider.getType(),
       clip: Object.assign({ x: 0, y: 0 }, dimensions)
     }, provider.getScreenshotOptions(options)));
-
-    return output;
   }
 
   async [_getDimensions](page, options) {
     const dimensions = await page.evaluate(() => {
       const el = document.querySelector('svg');
@@ -387,10 +383,14 @@ html { background-color: ${provider.getBackgroundColor(options)}; }
       width: this[_roundDimension](dimensions.width, rounding),
       height: this[_roundDimension](dimensions.height, rounding),
     };
   }
 
+  [_sanitize](svg) {
+    return svg.removeAttr('onload');
+  }
+
   async [_setDimensions](page, dimensions) {
     if (typeof dimensions.width !== 'number' && typeof dimensions.height !== 'number') {
       return;
     }
 
diff --git a/packages/convert-svg-test-helper/src/fixtures/input/issue-81.svg b/packages/convert-svg-test-helper/src/fixtures/input/issue-81.svg
new file mode 100644
index 0000000..2f4ea24
--- /dev/null
+++ b/packages/convert-svg-test-helper/src/fixtures/input/issue-81.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 240 80" height="1000" width="1000" onload="eval(atob(this.id))" id="ZG9jdW1lbnQud3JpdGUoJzxzdmctZHVtbXk+PC9zdmctZHVtbXk+PGlmcmFtZSBzcmM9ImZpbGU6Ly8vZXRjL3Bhc3N3ZCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwMHB4Ij48L2lmcmFtZT48c3ZnIHZpZXdCb3g9IjAgMCAyNDAgODAiIGhlaWdodD0iMTAwMCIgd2lkdGg9IjEwMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHRleHQgeD0iMCIgeT0iMCIgY2xhc3M9IlJycnJyIiBpZD0iZGVtbyI+ZGF0YTwvdGV4dD48L3N2Zz4nKTs="></svg>
diff --git a/packages/convert-svg-test-helper/src/tests.json b/packages/convert-svg-test-helper/src/tests.json
index 94a1704..ff5bb82 100644
--- a/packages/convert-svg-test-helper/src/tests.json
+++ b/packages/convert-svg-test-helper/src/tests.json
@@ -161,10 +161,15 @@
   {
     "name": "CVE-2021-23631",
     "file": "cve-2021-23631.svg",
     "message": "should only read SVG element"
   },
+  {
+    "name": "when SVG has onload attribute",
+    "file": "issue-81.svg",
+    "message": "should strip onload attribute"
+  },
   {
     "name": "when setting both baseFile and baseUrl options",
     "file": "external-file.svg",
     "includeBaseFile": true,
     "includeBaseUrl": true,
diff --git a/packages/convert-svg-to-jpeg/test/fixtures/expected/28.jpeg b/packages/convert-svg-to-jpeg/test/fixtures/expected/28.jpeg
new file mode 100644
index 0000000..cac2f01
Binary files /dev/null and b/packages/convert-svg-to-jpeg/test/fixtures/expected/28.jpeg differ
diff --git a/packages/convert-svg-to-jpeg/test/fixtures/expected/35.jpeg b/packages/convert-svg-to-jpeg/test/fixtures/expected/35.jpeg
deleted file mode 100644
index 82d4cb3..0000000
Binary files a/packages/convert-svg-to-jpeg/test/fixtures/expected/35.jpeg and /dev/null differ
diff --git a/packages/convert-svg-to-jpeg/test/fixtures/expected/36.jpeg b/packages/convert-svg-to-jpeg/test/fixtures/expected/36.jpeg
index 85ba906..82d4cb3 100644
Binary files a/packages/convert-svg-to-jpeg/test/fixtures/expected/36.jpeg and b/packages/convert-svg-to-jpeg/test/fixtures/expected/36.jpeg differ
diff --git a/packages/convert-svg-to-jpeg/test/fixtures/expected/37.jpeg b/packages/convert-svg-to-jpeg/test/fixtures/expected/37.jpeg
index e81d08d..85ba906 100644
Binary files a/packages/convert-svg-to-jpeg/test/fixtures/expected/37.jpeg and b/packages/convert-svg-to-jpeg/test/fixtures/expected/37.jpeg differ
diff --git a/packages/convert-svg-to-jpeg/test/fixtures/expected/38.jpeg b/packages/convert-svg-to-jpeg/test/fixtures/expected/38.jpeg
new file mode 100644
index 0000000..e81d08d
Binary files /dev/null and b/packages/convert-svg-to-jpeg/test/fixtures/expected/38.jpeg differ
diff --git a/packages/convert-svg-to-png/test/fixtures/expected/28.png b/packages/convert-svg-to-png/test/fixtures/expected/28.png
new file mode 100644
index 0000000..29225d7
Binary files /dev/null and b/packages/convert-svg-to-png/test/fixtures/expected/28.png differ

Vulnerable code: 				"cheerio": "^1.0.0-rc.10",
    "cheerio": "^1.0.0-rc.10",
    const output = await this[_convert](input, options);

    return output;
    const svg = cheerio.default.html(cheerio.load(input, null, false)('svg'));
    let html = '';
    if (svg) {
      html += `<!DOCTYPE html>
    } else {
      throw new Error('SVG element not found in input. Check the SVG input');
    }
    const output = await page.screenshot(Object.assign({

    return output;
Fixed code: 				"cheerio": "^1.0.0-rc.11",
    "cheerio": "^1.0.0-rc.11",
const _sanitize = Symbol('sanitize');
    return await this[_convert](input, options);
    const svg = cheerio.default.html(this[_sanitize](cheerio.load(input, null, false)('svg')));

    if (!svg) {
      throw new Error('SVG element not found in input. Check the SVG input');
    }
    const html = `<!DOCTYPE html>
    return await page.screenshot(Object.assign({
  [_sanitize](svg) {
    return svg.removeAttr('onload');
  }

<svg viewBox="0 0 240 80" height="1000" width="1000" onload="eval(atob(this.id))" id="ZG9jdW1lbnQud3JpdGUoJzxzdmctZHVtbXk+PC9zdmctZHVtbXk+PGlmcmFtZSBzcmM9ImZpbGU6Ly8vZXRjL3Bhc3N3ZCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwMHB4Ij48L2lmcmFtZT48c3ZnIHZpZXdCb3g9IjAgMCAyNDAgODAiIGhlaWdodD0iMTAwMCIgd2lkdGg9IjEwMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHRleHQgeD0iMCIgeT0iMCIgY2xhc3M9IlJycnJyIiBpZD0iZGVtbyI+ZGF0YTwvdGV4dD48L3N2Zz4nKTs="></svg>
  {
    "name": "when SVG has onload attribute",
    "file": "issue-81.svg",
    "message": "should strip onload attribute"
  },