From 08adc2bbcb13da18a4c0bb9ce1acfbd1f8539fcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=C3=A4fsten?= Date: Sun, 1 Nov 2026 15:43:55 +0100 Subject: [PATCH 1/2] fix(deps): bump lockfile to patch Dependabot alerts Updates transitive dependencies to patched releases: - follow-redirects 1.16.0 -> 1.16.2 (runtime, via axios; #131, #132) - fast-uri 3.1.8 -> 3.1.9 (dev, via ajv; #133-#136) - serialize-javascript 7.1.2 -> 7.1.4 (dev, via mocha; #137) - picomatch 2.3.1 -> 2.3.2 (dev; #48) Lockfile-only change; package.json ranges already allowed these versions. extract-zip (#120, #123) has no upstream fix and is left for a separate change. --- package-lock.json | 48 +++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index b6d87f99..038a13ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2780,9 +2780,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", - "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", "dev": true, "funding": [ { @@ -2889,9 +2889,9 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.2.tgz", + "integrity": "sha512-/kVP2U8z596Ia0gwFrwyIeo74ta5aVmSeU1FfAUMAmcEP5EP7zydmag3SLiN64NaRKef3lxsq7i/g7ltJQDGow==", "funding": [ { "type": "individual", @@ -4014,9 +4014,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -4223,9 +4223,9 @@ } }, "node_modules/serialize-javascript": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.2.tgz", - "integrity": "sha512-GL2BWwVa6JydKO6l/ljVgjAZF4QJ3S7dWDWi53s5GZT8PJzD2fNxi67HANQGKAqPrwEpL5ba7gUBGi0Ls/sEoQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.4.tgz", + "integrity": "sha512-FqlEuoTq/zRmA4Lg6aOqyH3lCHbUtCB0bmHCeFniX2GBdDqMJM3FwdvEhlE5Pim1dlt2FfBdVPmUNNiGhhKqpw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -6388,9 +6388,9 @@ "dev": true }, "fast-uri": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", - "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", "dev": true }, "fastq": { @@ -6463,9 +6463,9 @@ "dev": true }, "follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==" + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.2.tgz", + "integrity": "sha512-/kVP2U8z596Ia0gwFrwyIeo74ta5aVmSeU1FfAUMAmcEP5EP7zydmag3SLiN64NaRKef3lxsq7i/g7ltJQDGow==" }, "foreground-child": { "version": "3.3.1", @@ -7190,9 +7190,9 @@ "dev": true }, "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true }, "pidusage": { @@ -7305,9 +7305,9 @@ "dev": true }, "serialize-javascript": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.2.tgz", - "integrity": "sha512-GL2BWwVa6JydKO6l/ljVgjAZF4QJ3S7dWDWi53s5GZT8PJzD2fNxi67HANQGKAqPrwEpL5ba7gUBGi0Ls/sEoQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.4.tgz", + "integrity": "sha512-FqlEuoTq/zRmA4Lg6aOqyH3lCHbUtCB0bmHCeFniX2GBdDqMJM3FwdvEhlE5Pim1dlt2FfBdVPmUNNiGhhKqpw==", "dev": true }, "shebang-command": { From 7bc2cad89d4da8eeb8455b432b592a00dc25bc79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=C3=A4fsten?= Date: Sun, 1 Nov 2026 18:45:52 +0100 Subject: [PATCH 2/2] fix(deps): drop extract-zip for the system zip extractor extract-zip has unpatched symlink path-traversal advisories (#120, #123) and no upstream fix. It was only used by build scripts to unpack the CLI and CWF archives. Share one extractZipArchive helper between bundle-cli and update-cwf. It uses PowerShell Expand-Archive on Windows (as bundle-cli already did) and unzip elsewhere, then rejects any symlink that resolves outside the destination directory. Removing the dependency also drops yauzl and its transitive packages from the lockfile. --- package-lock.json | 221 ----------------------------- package.json | 1 - scripts/bundle-cli.js | 19 +-- scripts/extract-zip.js | 49 +++++++ scripts/update-cwf.js | 4 +- src/test/suite/extract-zip.test.ts | 120 ++++++++++++++++ 6 files changed, 172 insertions(+), 242 deletions(-) create mode 100644 scripts/extract-zip.js create mode 100644 src/test/suite/extract-zip.test.ts diff --git a/package-lock.json b/package-lock.json index 038a13ca..32f51a58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,6 @@ "esbuild": "^0.28.2", "esbuild-plugin-copy": "^2.1.1", "eslint": "^10.11.0", - "extract-zip": "^2.0.1", "follow-redirects": "^1.16.0", "husky": "^9.1.7", "mocha": "^12.0.2", @@ -1400,17 +1399,6 @@ "integrity": "sha512-iBAUYNYkz+uk1kdsq05fEcoh8gJmwT3lqqFPN7MGyjQ3HVloViMdo7ZJ8DFIP8WOK74PjOEilosqAyxV2iUFUw==", "license": "MIT" }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.71.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.0.tgz", @@ -1875,16 +1863,6 @@ "dev": true, "license": "ISC" }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/c8": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/c8/-/c8-12.0.0.tgz", @@ -2294,16 +2272,6 @@ "node": ">= 0.4" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -2720,27 +2688,6 @@ "node": ">=0.10.0" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2806,16 +2753,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-entry-cache": { "version": "11.1.5", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", @@ -3040,22 +2977,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -3869,16 +3790,6 @@ "node": ">=0.10.0" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3999,13 +3910,6 @@ "node": ">=8" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4079,17 +3983,6 @@ "node": ">=10" } }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4565,13 +4458,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -4656,17 +4542,6 @@ "node": "^20.19.0 || ^22.12.0 || >=23" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -5507,16 +5382,6 @@ "resolved": "https://registry.npmjs.org/@types/vscode-webview/-/vscode-webview-1.57.5.tgz", "integrity": "sha512-iBAUYNYkz+uk1kdsq05fEcoh8gJmwT3lqqFPN7MGyjQ3HVloViMdo7ZJ8DFIP8WOK74PjOEilosqAyxV2iUFUw==" }, - "@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "requires": { - "@types/node": "*" - } - }, "@typescript-eslint/eslint-plugin": { "version": "8.71.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.0.tgz", @@ -5795,12 +5660,6 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true - }, "c8": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/c8/-/c8-12.0.0.tgz", @@ -6063,15 +5922,6 @@ "gopd": "^1.2.0" } }, - "end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, "env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -6344,18 +6194,6 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, - "extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "requires": { - "@types/yauzl": "^2.9.1", - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - } - }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6402,15 +6240,6 @@ "reusify": "^1.0.4" } }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "requires": { - "pend": "~1.2.0" - } - }, "file-entry-cache": { "version": "11.1.5", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", @@ -6550,15 +6379,6 @@ "es-object-atoms": "^1.0.0" } }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, "glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -7091,15 +6911,6 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "requires": { - "wrappy": "1" - } - }, "optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -7177,12 +6988,6 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, "picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7223,16 +7028,6 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==" }, - "pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -7514,12 +7309,6 @@ "integrity": "sha512-6z2Iis68Wqth93/G/wJP9u+R3O+d2XTlgWChGCwuT1qLbBsOYueGRZuJ++v3mtDP5KjYdy+WzvWC+VWETSVXJA==", "dev": true }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -7573,16 +7362,6 @@ } } }, - "yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "requires": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index ac7b5f4e..c3e972e1 100644 --- a/package.json +++ b/package.json @@ -261,7 +261,6 @@ "esbuild": "^0.28.2", "esbuild-plugin-copy": "^2.1.1", "eslint": "^10.11.0", - "extract-zip": "^2.0.1", "follow-redirects": "^1.16.0", "husky": "^9.1.7", "mocha": "^12.0.2", diff --git a/scripts/bundle-cli.js b/scripts/bundle-cli.js index 32b875ef..d540cf71 100755 --- a/scripts/bundle-cli.js +++ b/scripts/bundle-cli.js @@ -7,15 +7,11 @@ */ const { https } = require('follow-redirects'); -const { execFile } = require('child_process'); -const { promisify } = require('util'); const fs = require('fs'); const path = require('path'); -const extractZip = require('extract-zip'); +const { extractZipArchive } = require('./extract-zip.js'); const { artifacts, nativeBinaryFileName, requiredSidecarFileNames } = require('./cli-config.js'); -const execFileAsync = promisify(execFile); - function getDistributionName(platform, arch) { return `cs-${platform}-${arch}`; } @@ -146,19 +142,6 @@ async function removePath(filePath) { } catch (e) {} } -async function extractZipArchive(zipPath, destinationDir) { - // Windows CI publishes zips via Compress-Archive; extract-zip/yauzl can stop early on those. - if (process.platform === 'win32') { - await execFileAsync( - 'powershell.exe', - ['-NoProfile', '-Command', `Expand-Archive -LiteralPath '${zipPath.replace(/'/g, "''")}' -DestinationPath '${destinationDir.replace(/'/g, "''")}' -Force`], - { windowsHide: true, maxBuffer: 10 * 1024 * 1024 } - ); - return; - } - await extractZip(zipPath, { dir: destinationDir }); -} - async function extractDistribution(zipPath, platform, arch) { const projectRoot = path.join(__dirname, '..'); const tempExtractDir = path.join(projectRoot, `.temp-extract-${platform}-${arch}`); diff --git a/scripts/extract-zip.js b/scripts/extract-zip.js new file mode 100644 index 00000000..37212d5a --- /dev/null +++ b/scripts/extract-zip.js @@ -0,0 +1,49 @@ +const { execFile } = require('child_process'); +const { promisify } = require('util'); +const fs = require('fs'); +const path = require('path'); + +const execFileAsync = promisify(execFile); +const execOptions = { windowsHide: true, maxBuffer: 10 * 1024 * 1024 }; + +function isInside(root, candidate) { + const relative = path.relative(root, candidate); + return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative)); +} + +async function assertNoEscapingLinks(root, dir = root) { + for (const entry of await fs.promises.readdir(dir, { withFileTypes: true })) { + const entryPath = path.join(dir, entry.name); + if (entry.isSymbolicLink()) { + const target = path.resolve(dir, await fs.promises.readlink(entryPath)); + if (!isInside(root, target)) { + throw new Error(`Archive entry ${path.relative(root, entryPath)} links outside the extraction directory`); + } + } else if (entry.isDirectory()) { + await assertNoEscapingLinks(root, entryPath); + } + } +} + +async function runExtractor(zipPath, destinationDir) { + // Windows CI publishes zips via Compress-Archive; Expand-Archive reads those reliably. + if (process.platform === 'win32') { + const quote = (value) => value.replace(/'/g, "''"); + await execFileAsync( + 'powershell.exe', + ['-NoProfile', '-Command', `Expand-Archive -LiteralPath '${quote(zipPath)}' -DestinationPath '${quote(destinationDir)}' -Force`], + execOptions + ); + return; + } + await execFileAsync('unzip', ['-q', '-o', zipPath, '-d', destinationDir], execOptions); +} + +async function extractZipArchive(zipPath, destinationDir) { + const root = path.resolve(destinationDir); + await fs.promises.mkdir(root, { recursive: true }); + await runExtractor(path.resolve(zipPath), root); + await assertNoEscapingLinks(root); +} + +module.exports = { extractZipArchive }; diff --git a/scripts/update-cwf.js b/scripts/update-cwf.js index d8e6cf94..3b6f5202 100755 --- a/scripts/update-cwf.js +++ b/scripts/update-cwf.js @@ -4,7 +4,7 @@ const { execSync } = require('child_process'); const fs = require('fs'); const os = require('os'); const path = require('path'); -const extractZip = require('extract-zip'); +const { extractZipArchive } = require('./extract-zip.js'); const projectRoot = path.join(__dirname, '..'); const localCwfDir = path.join(projectRoot, '..', 'cs-webview'); @@ -80,7 +80,7 @@ async function extractCwf(zipPath, dir) { fs.rmSync(path.join(dir, stampFileName), { force: true }); fs.rmSync(dir, { recursive: true, force: true }); fs.mkdirSync(dir, { recursive: true }); - await extractZip(zipPath, { dir: path.resolve(dir) }); + await extractZipArchive(zipPath, dir); const missing = requiredFiles.filter((file) => !fs.existsSync(path.join(dir, file))); if (missing.length > 0) { throw new Error(`CWF archive is missing ${missing.map((file) => `cs-cwf/${file}`).join(', ')}`); diff --git a/src/test/suite/extract-zip.test.ts b/src/test/suite/extract-zip.test.ts new file mode 100644 index 00000000..0b4f2351 --- /dev/null +++ b/src/test/suite/extract-zip.test.ts @@ -0,0 +1,120 @@ +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import * as zlib from 'zlib'; + +const { extractZipArchive } = require('../../../scripts/extract-zip'); + +interface ZipEntry { + name: string; + data: string; + unixMode?: number; +} + +function buildZip(entries: ZipEntry[]): Buffer { + const parts: Buffer[] = []; + const central: Buffer[] = []; + let offset = 0; + for (const entry of entries) { + const name = Buffer.from(entry.name); + const data = Buffer.from(entry.data); + const crc = zlib.crc32(data); + const local = Buffer.alloc(30); + local.writeUInt32LE(0x04034b50, 0); + local.writeUInt16LE(10, 4); + local.writeUInt32LE(crc, 14); + local.writeUInt32LE(data.length, 18); + local.writeUInt32LE(data.length, 22); + local.writeUInt16LE(name.length, 26); + const header = Buffer.alloc(46); + header.writeUInt32LE(0x02014b50, 0); + header.writeUInt16LE(0x031e, 4); + header.writeUInt16LE(10, 6); + header.writeUInt32LE(crc, 16); + header.writeUInt32LE(data.length, 20); + header.writeUInt32LE(data.length, 24); + header.writeUInt16LE(name.length, 28); + header.writeUInt32LE(((entry.unixMode ?? 0o100644) << 16) >>> 0, 38); + header.writeUInt32LE(offset, 42); + central.push(Buffer.concat([header, name])); + parts.push(local, name, data); + offset += local.length + name.length + data.length; + } + const centralDirectory = Buffer.concat(central); + const end = Buffer.alloc(22); + end.writeUInt32LE(0x06054b50, 0); + end.writeUInt16LE(entries.length, 8); + end.writeUInt16LE(entries.length, 10); + end.writeUInt32LE(centralDirectory.length, 12); + end.writeUInt32LE(offset, 16); + return Buffer.concat([...parts, centralDirectory, end]); +} + +suite('Extract Zip Script Test Suite', () => { + const directories: string[] = []; + + function tempDir(): string { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'cs-extract-zip-')); + directories.push(directory); + return directory; + } + + function writeZip(entries: ZipEntry[]): string { + const zipPath = path.join(tempDir(), 'archive.zip'); + fs.writeFileSync(zipPath, buildZip(entries)); + return zipPath; + } + + teardown(() => { + for (const directory of directories.splice(0)) { + fs.rmSync(directory, { recursive: true, force: true }); + } + }); + + test('extracts nested files and creates the destination directory', async () => { + const zipPath = writeZip([ + { name: 'index.html', data: '' }, + { name: 'assets/index.js', data: 'console.log(1);' }, + ]); + const destination = path.join(tempDir(), 'out', 'nested'); + + await extractZipArchive(zipPath, destination); + + assert.strictEqual(fs.readFileSync(path.join(destination, 'index.html'), 'utf8'), ''); + assert.strictEqual(fs.readFileSync(path.join(destination, 'assets', 'index.js'), 'utf8'), 'console.log(1);'); + }); + + test('rejects an archive that has not been created', async () => { + const destination = tempDir(); + await assert.rejects(extractZipArchive(path.join(destination, 'missing.zip'), destination)); + }); + + const symlinkCases = [ + { description: 'a symlink pointing outside the destination', target: '../outside', accepted: false }, + { description: 'an absolute symlink pointing outside the destination', target: os.tmpdir(), accepted: false }, + { description: 'a symlink staying inside the destination', target: 'real.txt', accepted: true }, + ]; + + for (const { description, target, accepted } of symlinkCases) { + test(`${accepted ? 'keeps' : 'rejects'} ${description}`, async function () { + if (process.platform === 'win32') { + this.skip(); + } + const zipPath = writeZip([ + { name: 'real.txt', data: 'content' }, + { name: 'link', data: target, unixMode: 0o120777 }, + ]); + const destination = path.join(tempDir(), 'out'); + + const extraction = extractZipArchive(zipPath, destination); + + if (accepted) { + await extraction; + assert.strictEqual(fs.readlinkSync(path.join(destination, 'link')), target); + } else { + await assert.rejects(extraction, /links outside the extraction directory/); + } + }); + } +});