CVE-2022-24278: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: The package convert-svg-core before 0.6.4 are vulnerable to Directory Traversal due to improper sanitization of SVG tags. Exploiting this vulnerability is possible by using a specially crafted SVG file.
CWE description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
CVSS v3 base score: 9.8
Published: 2022-06-10T20:15Z
Repository: https://github.com/neocotic/convert-svg
Language: JSON
Files changed: packages/convert-svg-core/src/Converter.js, packages/convert-svg-test-helper/src/fixtures/input/issue-86.svg, packages/convert-svg-test-helper/src/tests.json
Commit date: 2022-06-07 10:29:11 +0100
Commit message: Convert only first SVG element from input

If the input to be converted contains multiple SVG elements, only the first SVG element should be converted. Currently, and undesirably, all SVG elements were being inserted into the page within Puppeteer, resulting in them all being stacked prior to the screenshot being taken.

Another symptom of this was that all recent safety measures implemented for remote code injection vulnerabilities can be easily circumvented by inserting a simple empty SVG element at the start of the input as it was the only one being sanitized.

Diff: diff --git a/packages/convert-svg-core/src/Converter.js b/packages/convert-svg-core/src/Converter.js
index 05bd327..5af35b0 100644
--- a/packages/convert-svg-core/src/Converter.js
+++ b/packages/convert-svg-core/src/Converter.js
@@ -259,11 +259,11 @@ class Converter {
 
   async [_convert](input, options) {
     input = Buffer.isBuffer(input) ? input.toString('utf8') : input;
 
     const { provider } = this;
-    const svg = cheerio.default.html(this[_sanitize](cheerio.load(input, null, false)('svg'), options));
+    const svg = cheerio.default.html(this[_sanitize](cheerio.load(input, null, false)('svg:first'), options));
 
     if (!svg) {
       throw new Error('SVG element not found in input. Check the SVG input');
     }
 
diff --git a/packages/convert-svg-test-helper/src/fixtures/input/issue-86.svg b/packages/convert-svg-test-helper/src/fixtures/input/issue-86.svg
new file mode 100644
index 0000000..fce485e
--- /dev/null
+++ b/packages/convert-svg-test-helper/src/fixtures/input/issue-86.svg
@@ -0,0 +1,2 @@
+<svg></svg>
+<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 ff5bb82..b670143 100644
--- a/packages/convert-svg-test-helper/src/tests.json
+++ b/packages/convert-svg-test-helper/src/tests.json
@@ -166,10 +166,16 @@
   {
     "name": "when SVG has onload attribute",
     "file": "issue-81.svg",
     "message": "should strip onload attribute"
   },
+  {
+    "name": "when SVG has multiple SVG elements",
+    "file": "issue-86.svg",
+    "options": { "width": 1000, "height": 1000 },
+    "message": "should only read first SVG element"
+  },
   {
     "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/29.jpeg b/packages/convert-svg-to-jpeg/test/fixtures/expected/29.jpeg
new file mode 100644
index 0000000..cac2f01
Binary files /dev/null and b/packages/convert-svg-to-jpeg/test/fixtures/expected/29.jpeg 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
deleted file mode 100644
index 82d4cb3..0000000
Binary files a/packages/convert-svg-to-jpeg/test/fixtures/expected/36.jpeg and /dev/null 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 85ba906..82d4cb3 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
index e81d08d..85ba906 100644
Binary files a/packages/convert-svg-to-jpeg/test/fixtures/expected/38.jpeg and b/packages/convert-svg-to-jpeg/test/fixtures/expected/38.jpeg differ
diff --git a/packages/convert-svg-to-jpeg/test/fixtures/expected/39.jpeg b/packages/convert-svg-to-jpeg/test/fixtures/expected/39.jpeg
new file mode 100644
index 0000000..e81d08d
Binary files /dev/null and b/packages/convert-svg-to-jpeg/test/fixtures/expected/39.jpeg differ
diff --git a/packages/convert-svg-to-png/test/fixtures/expected/29.png b/packages/convert-svg-to-png/test/fixtures/expected/29.png
new file mode 100644
index 0000000..29225d7
Binary files /dev/null and b/packages/convert-svg-to-png/test/fixtures/expected/29.png differ
diff --git a/packages/convert-svg-to-webp/test/fixtures/expected/29.webp b/packages/convert-svg-to-webp/test/fixtures/expected/29.webp
new file mode 100644
index 0000000..38d2cfd
Binary files /dev/null and b/packages/convert-svg-to-webp/test/fixtures/expected/29.webp differ

Vulnerable code:     const svg = cheerio.default.html(this[_sanitize](cheerio.load(input, null, false)('svg'), options));
Fixed code:     const svg = cheerio.default.html(this[_sanitize](cheerio.load(input, null, false)('svg:first'), options));
<svg></svg>
<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 multiple SVG elements",
    "file": "issue-86.svg",
    "options": { "width": 1000, "height": 1000 },
    "message": "should only read first SVG element"
  },