From dc73ad760aa772d139bea252793bafc7189dbc7b Mon Sep 17 00:00:00 2001 From: Tatsat Mishra Date: Mon, 24 Aug 2026 21:33:06 +1200 Subject: [PATCH 1/2] fix(deps): replace unmaintained decompress with targeted archive extraction decompress@4.2.1 carries three critical advisories with no fix available; it has been unmaintained since 2017: GHSA-mp2f-45pm-3cg9 extraction can create files/links outside the target dir GHSA-h39j-r5qq-r9mm arbitrary file write via archive extraction (Zip Slip) GHSA-jwp9-9v96-94mx arbitrary hardlink creation during extraction Replaces it with tar (npm/isaacs) for .tar.gz and yauzl (the reader behind extract-zip, vsce and Electron) for .zip. Both already resolved in the tree transitively, so they dedupe and add no new packages. The vulnerability class is removed structurally rather than mitigated. The old code extracted the whole archive to disk and then moved the one file it wanted, so every path in the archive was written. This extracts only the requested entry, streamed directly to a path we compute ourselves, leaving a malicious archive no path it can influence. Two issues the rewrite would otherwise have introduced are handled: Archive entries are always '/'-separated, but pathToBinaryInArchive is built with path.join and yields '\' on Windows. toArchiveEntryPath() normalises before comparing; the old code sidestepped this by extracting to real filesystem paths. Writing straight to binaryFilePath would let a failed extraction leave a truncated binary for the existsSync cache check to return on the next run. Extraction now goes to a .partial file and renames only on success, with cleanup on failure. Archive format is inferred from the file extension, so both call sites are unchanged. An unrecognised extension fails with a clear error. Verified against real releases with the production getToolBinaryPath(): both kubelogin (.zip, nested entry) and kubectl-gadget (.tar.gz, root entry) extract byte-identical to unzip -p / tar xzO, land at mode 755, clean up the archive, and execute. npm audit drops from 3 criticals to 0. --- package-lock.json | 584 +----------------- package.json | 6 +- .../utils/helper/binaryDownloadHelper.ts | 131 +++- src/types/yauzl.d.ts | 50 ++ 4 files changed, 180 insertions(+), 591 deletions(-) create mode 100644 src/types/yauzl.d.ts diff --git a/package-lock.json b/package-lock.json index ca25c0b50..b3b25f499 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,6 @@ "@vscode/extension-telemetry": "^1.5.2", "@vscode/l10n": "^0.0.18", "containerization-assist-mcp": "^1.5.0", - "decompress": "^4.2.1", "js-yaml": "^5.2.3", "libsodium-wrappers": "^0.8.4", "move-file": "^4.1.0", @@ -42,15 +41,16 @@ "rxjs": "^7.8.2", "semver": "^7.8.5", "sinon": "^22.1.0", + "tar": "^7.5.21", "tmp": "^0.2.7", "uuid": "^14.0.0", - "vscode-kubernetes-tools-api": "^1.0.0" + "vscode-kubernetes-tools-api": "^1.0.0", + "yauzl": "^3.4.0" }, "devDependencies": { "@eslint/eslintrc": "^3.3.6", "@eslint/js": "^10.0.1", "@types/chai": "^5.2.3", - "@types/decompress": "^4.2.7", "@types/js-yaml": "^4.0.9", "@types/libsodium-wrappers": "^0.8.2", "@types/mocha": "^10.0.10", @@ -2463,16 +2463,6 @@ "assertion-error": "^2.0.1" } }, - "node_modules/@types/decompress": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.7.tgz", - "integrity": "sha512-9z+8yjKr5Wn73Pt17/ldnmQToaFHZxK0N1GHysuk/JIPT8RIdQeoInM01wWPgypRcvb6VH1drjuFpQ4zmY437g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -4039,21 +4029,6 @@ "node": ">=8.0.0" } }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/azure-devops-node-api": { "version": "12.5.0", "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", @@ -4252,52 +4227,6 @@ "url": "https://bevry.me/fund" } }, - "node_modules/bl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", - "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", - "license": "MIT", - "dependencies": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/bl/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/bl/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/bl/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/bl/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -4441,26 +4370,11 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "license": "MIT", - "dependencies": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "node_modules/buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", - "license": "MIT" - }, "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": "*" @@ -4472,12 +4386,6 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, - "node_modules/buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", - "license": "MIT" - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -4518,24 +4426,6 @@ "node": ">= 0.8" } }, - "node_modules/call-bind": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", - "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "get-intrinsic": "^1.3.0", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -5140,6 +5030,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, "license": "MIT" }, "node_modules/cors": { @@ -5283,25 +5174,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/decompress": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", - "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", - "license": "MIT", - "dependencies": { - "decompress-tar": "^4.0.0", - "decompress-tarbz2": "^4.0.0", - "decompress-targz": "^4.0.0", - "decompress-unzip": "^4.0.1", - "graceful-fs": "^4.1.10", - "make-dir": "^1.0.0", - "pify": "^2.3.0", - "strip-dirs": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -5319,93 +5191,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/decompress-tar": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", - "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", - "license": "MIT", - "dependencies": { - "file-type": "^5.2.0", - "is-stream": "^1.1.0", - "tar-stream": "^1.5.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-tarbz2": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", - "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", - "license": "MIT", - "dependencies": { - "decompress-tar": "^4.1.0", - "file-type": "^6.1.0", - "is-stream": "^1.1.0", - "seek-bzip": "^1.0.5", - "unbzip2-stream": "^1.0.9" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-tarbz2/node_modules/file-type": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", - "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-targz": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", - "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", - "license": "MIT", - "dependencies": { - "decompress-tar": "^4.1.1", - "file-type": "^5.2.0", - "is-stream": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-unzip": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", - "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", - "license": "MIT", - "dependencies": { - "file-type": "^3.8.0", - "get-stream": "^2.2.0", - "pify": "^2.3.0", - "yauzl": "^2.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-unzip/node_modules/file-type": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", - "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decompress-unzip/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==", - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -5471,23 +5256,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", @@ -6823,15 +6591,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==", - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -6886,15 +6645,6 @@ "node": ">=16.0.0" } }, - "node_modules/file-type": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -6977,21 +6727,6 @@ "devOptional": true, "license": "ISC" }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -7174,19 +6909,6 @@ "node": ">=8" } }, - "node_modules/get-stream": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", - "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", - "license": "MIT", - "dependencies": { - "object-assign": "^4.0.1", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -7325,6 +7047,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/has-flag": { @@ -7337,18 +7060,6 @@ "node": ">=8" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -7720,18 +7431,6 @@ "node": ">= 0.10" } }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-cidr": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/is-cidr/-/is-cidr-4.0.2.tgz", @@ -7829,12 +7528,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-natural-number": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", - "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", - "license": "MIT" - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -7880,30 +7573,6 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, - "node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -7944,12 +7613,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "license": "MIT" - }, "node_modules/isexe": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", @@ -8547,27 +8210,6 @@ "node": ">=10" } }, - "node_modules/make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "license": "MIT", - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/make-dir/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/markdown-it": { "version": "14.2.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", @@ -9841,36 +9483,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", - "license": "MIT", - "dependencies": { - "pinkie": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pino": { "version": "9.14.0", "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", @@ -9996,15 +9608,6 @@ "node": ">=4" } }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -10117,6 +9720,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, "license": "MIT" }, "node_modules/process-warning": { @@ -10790,25 +10394,6 @@ "node": ">=20.0.0" } }, - "node_modules/seek-bzip": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", - "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", - "license": "MIT", - "dependencies": { - "commander": "^2.8.1" - }, - "bin": { - "seek-bunzip": "bin/seek-bunzip", - "seek-table": "bin/seek-bzip-table" - } - }, - "node_modules/seek-bzip/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" - }, "node_modules/selderee": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", @@ -10894,23 +10479,6 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "license": "ISC" }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -11489,15 +11057,6 @@ "node": ">=8" } }, - "node_modules/strip-dirs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", - "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", - "license": "MIT", - "dependencies": { - "is-natural-number": "^4.0.1" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -11703,60 +11262,6 @@ "streamx": "^2.15.0" } }, - "node_modules/tar-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", - "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", - "license": "MIT", - "dependencies": { - "bl": "^1.0.0", - "buffer-alloc": "^1.2.0", - "end-of-stream": "^1.0.0", - "fs-constants": "^1.0.0", - "readable-stream": "^2.3.0", - "to-buffer": "^1.1.1", - "xtend": "^4.0.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/tar-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/tar-stream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/tar-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/tar-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -11868,12 +11373,6 @@ "real-require": "^0.2.0" } }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "license": "MIT" - }, "node_modules/tinyexec": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", @@ -11910,20 +11409,6 @@ "node": ">=14.14" } }, - "node_modules/to-buffer": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", - "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", - "license": "MIT", - "dependencies": { - "isarray": "^2.0.5", - "safe-buffer": "^5.2.1", - "typed-array-buffer": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -12089,20 +11574,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/typed-rest-client": { "version": "1.8.11", "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", @@ -12136,16 +11607,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unbzip2-stream": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", - "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", - "license": "MIT", - "dependencies": { - "buffer": "^5.2.1", - "through": "^2.3.8" - } - }, "node_modules/underscore": { "version": "1.13.8", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", @@ -12590,27 +12051,6 @@ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "license": "ISC" }, - "node_modules/which-typed-array": { - "version": "1.1.22", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", - "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", @@ -12817,15 +12257,6 @@ "node": ">=4.0" } }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -12956,7 +12387,6 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", - "dev": true, "license": "MIT", "dependencies": { "pend": "~1.2.0" diff --git a/package.json b/package.json index 8d5f654fa..a1f6f815b 100644 --- a/package.json +++ b/package.json @@ -1130,7 +1130,6 @@ "@eslint/eslintrc": "^3.3.6", "@eslint/js": "^10.0.1", "@types/chai": "^5.2.3", - "@types/decompress": "^4.2.7", "@types/js-yaml": "^4.0.9", "@types/libsodium-wrappers": "^0.8.2", "@types/mocha": "^10.0.10", @@ -1201,7 +1200,6 @@ "@vscode/extension-telemetry": "^1.5.2", "@vscode/l10n": "^0.0.18", "containerization-assist-mcp": "^1.5.0", - "decompress": "^4.2.1", "js-yaml": "^5.2.3", "libsodium-wrappers": "^0.8.4", "move-file": "^4.1.0", @@ -1209,9 +1207,11 @@ "rxjs": "^7.8.2", "semver": "^7.8.5", "sinon": "^22.1.0", + "tar": "^7.5.21", "tmp": "^0.2.7", "uuid": "^14.0.0", - "vscode-kubernetes-tools-api": "^1.0.0" + "vscode-kubernetes-tools-api": "^1.0.0", + "yauzl": "^3.4.0" }, "repository": { "type": "git", diff --git a/src/commands/utils/helper/binaryDownloadHelper.ts b/src/commands/utils/helper/binaryDownloadHelper.ts index 9c6ab4eb3..52e3eda05 100644 --- a/src/commands/utils/helper/binaryDownloadHelper.ts +++ b/src/commands/utils/helper/binaryDownloadHelper.ts @@ -4,6 +4,9 @@ import * as fs from "fs"; import { moveFile } from "move-file"; import { Errorable, failed } from "../errorable"; import path from "path"; +import { pipeline } from "stream/promises"; +import * as tar from "tar"; +import * as yauzl from "yauzl"; import { longRunning } from "../host"; function getToolBaseInstallFolder(toolName: string): string { @@ -37,6 +40,109 @@ function isArchive(downloadSpec: DownloadSpec): downloadSpec is ArchiveDownloadS return downloadSpec.isCompressed; } +/** + * Archive entry paths are always '/'-separated, but `pathToBinaryInArchive` is + * built with `path.join`, which yields '\' on Windows. Compare in one form. + */ +function toArchiveEntryPath(filePath: string): string { + return filePath.split(path.sep).join("/").replace(/^\.\//, ""); +} + +/** + * Extracts a single known entry from a zip, streaming it straight to `destPath`. + * + * Only the entry we asked for is written, and it is written to a path we + * computed ourselves, so a malicious archive cannot direct a write elsewhere + * (the Zip Slip / hardlink class of bug). + */ +function extractFileFromZip(archivePath: string, entryPath: string, destPath: string): Promise { + const wantedEntry = toArchiveEntryPath(entryPath); + + return new Promise((resolve, reject) => { + yauzl.open(archivePath, { lazyEntries: true }, (openError, zipFile) => { + if (openError || !zipFile) { + reject(openError ?? new Error("Failed to open archive.")); + return; + } + + let found = false; + + zipFile.on("error", reject); + + zipFile.on("entry", (entry: yauzl.Entry) => { + if (toArchiveEntryPath(entry.fileName) !== wantedEntry) { + zipFile.readEntry(); + return; + } + + found = true; + zipFile.openReadStream(entry, (streamError, readStream) => { + if (streamError || !readStream) { + reject(streamError ?? new Error(`Failed to read ${wantedEntry} from archive.`)); + return; + } + + pipeline(readStream, fs.createWriteStream(destPath)).then(() => { + zipFile.close(); + resolve(); + }, reject); + }); + }); + + zipFile.on("end", () => { + if (!found) { + reject(new Error(`Archive does not contain an entry named ${wantedEntry}.`)); + } + }); + + zipFile.readEntry(); + }); + }); +} + +/** + * Extracts a single known entry from a gzipped tarball. Same guarantee as + * `extractFileFromZip`: one entry, written only to `destPath`. + */ +async function extractFileFromTarball(archivePath: string, entryPath: string, destPath: string): Promise { + const wantedEntry = toArchiveEntryPath(entryPath); + let writeCompleted: Promise | undefined; + + await tar.list({ + file: archivePath, + onReadEntry: (entry) => { + if (toArchiveEntryPath(entry.path) !== wantedEntry) { + // Leave it unconsumed; tar drains entries we don't attach to. + return; + } + + writeCompleted = pipeline(entry, fs.createWriteStream(destPath)); + }, + }); + + if (!writeCompleted) { + throw new Error(`Archive does not contain an entry named ${wantedEntry}.`); + } + + await writeCompleted; +} + +async function extractBinaryFromArchive(archivePath: string, entryPath: string, destPath: string): Promise { + const lowerCaseArchivePath = archivePath.toLowerCase(); + + if (lowerCaseArchivePath.endsWith(".zip")) { + await extractFileFromZip(archivePath, entryPath, destPath); + return; + } + + if (lowerCaseArchivePath.endsWith(".tar.gz") || lowerCaseArchivePath.endsWith(".tgz")) { + await extractFileFromTarball(archivePath, entryPath, destPath); + return; + } + + throw new Error(`Unsupported archive format: ${path.basename(archivePath)}. Expected .zip, .tar.gz or .tgz.`); +} + export async function getToolBinaryPath( toolName: string, version: string, @@ -51,12 +157,13 @@ export async function getToolBinaryPath( } return await longRunning(`Downloading ${toolName} to ${binaryFilePath}.`, () => - downloadTool(toolName, binaryFilePath, downloadSpec), + downloadTool(toolName, binaryFolder, binaryFilePath, downloadSpec), ); } async function downloadTool( toolName: string, + binaryFolder: string, binaryFilePath: string, downloadSpec: DownloadSpec, ): Promise> { @@ -73,23 +180,25 @@ async function downloadTool( } if (isArchive(downloadSpec)) { - const { default: decompress } = await import("decompress"); + // Extract to a temporary path alongside the destination and rename only on + // success, so a failed extraction can't leave a truncated binary at + // `binaryFilePath` for the `existsSync` check above to find next time. + fs.mkdirSync(binaryFolder, { recursive: true }); + const partialFilePath = `${binaryFilePath}.partial`; try { - await decompress(downloadFilePath, downloadFolder); + await extractBinaryFromArchive(downloadFilePath, downloadSpec.pathToBinaryInArchive, partialFilePath); + fs.renameSync(partialFilePath, binaryFilePath); } catch (error) { + fs.rmSync(partialFilePath, { force: true }); return { succeeded: false, - error: `Failed to unzip binary ${downloadFilePath} to ${downloadFolder}: ${error}`, + error: `Failed to extract ${downloadSpec.pathToBinaryInArchive} from ${downloadFilePath}: ${error}`, }; + } finally { + // Remove the archive whether or not extraction succeeded. + fs.rmSync(downloadFilePath, { force: true }); } - - // Remove zip. - fs.unlinkSync(downloadFilePath); - - // Move extracted binary to where we want it. - const unzippedBinaryFilePath = path.join(downloadFolder, downloadSpec.pathToBinaryInArchive); - await moveFile(unzippedBinaryFilePath, binaryFilePath); } else { await moveFile(downloadFilePath, binaryFilePath); } diff --git a/src/types/yauzl.d.ts b/src/types/yauzl.d.ts new file mode 100644 index 000000000..f1d1c17b1 --- /dev/null +++ b/src/types/yauzl.d.ts @@ -0,0 +1,50 @@ +/** + * Minimal ambient types for `yauzl`, which ships no types of its own. + * + * Declared locally rather than depending on `@types/yauzl` so this extension + * adds no new packages to the dependency tree. Only the surface actually used + * by `binaryDownloadHelper.ts` is described here — extend it if more of the + * API is needed. + * + * Upstream API reference: https://github.com/thejoshwolfe/yauzl#api + */ +declare module "yauzl" { + import { Readable } from "stream"; + + /** A single entry in the zip's central directory. */ + export interface Entry { + /** Always '/'-separated, per the zip specification. */ + fileName: string; + uncompressedSize: number; + compressedSize: number; + } + + export interface ZipFile { + /** Emits the next entry; only meaningful with `lazyEntries: true`. */ + on(event: "entry", listener: (entry: Entry) => void): this; + /** All entries have been read. */ + on(event: "end", listener: () => void): this; + on(event: "error", listener: (error: Error) => void): this; + /** Requests the next entry. Required when `lazyEntries` is set. */ + readEntry(): void; + openReadStream(entry: Entry, callback: (error: Error | null, stream?: Readable) => void): void; + close(): void; + } + + export interface OpenOptions { + /** + * When true, entries are emitted one at a time in response to + * `readEntry()` rather than all at once. + */ + lazyEntries?: boolean; + autoClose?: boolean; + decodeStrings?: boolean; + validateEntrySizes?: boolean; + } + + export function open( + path: string, + options: OpenOptions, + callback: (error: Error | null, zipFile?: ZipFile) => void, + ): void; +} From f7858c27abe52e83270e967173f7625933ac49ab Mon Sep 17 00:00:00 2001 From: Tatsat Mishra Date: Thu, 27 Aug 2026 10:30:38 +1200 Subject: [PATCH 2/2] fix(deps): address review - close zip handle, guard tar duplicates, keep retries working Three issues raised in review on #2393. download.clear() was skipped by the early return in the catch, while the finally still deleted the archive. That left download.once() holding a Completed marker for a file that no longer existed, so every retry skipped the download and failed on the missing archive until the window was restarted. On main the archive survived a failed extraction, so retries still worked - this was a regression introduced by the finally block. Reproduced, then fixed by clearing the marker alongside the removal. The zip reader only closed the ZipFile on the success path. Every error path - openReadStream failure, pipeline rejection, and entry-not-found - left the handle open, leaking a descriptor and, on Windows, blocking deletion of the archive in the caller's finally. Settling now goes through a single idempotent finish() that always closes first. Verified with lsof: 12 consecutive failed extractions leave the descriptor count unchanged. extractFileFromTarball reassigned writeCompleted on every matching entry. A tar is append-only, so the same path may legally appear twice; attaching twice started two concurrent writes to the same destination. Built an archive containing "payload" twice: the previous code emitted the second copy, the guarded version deterministically takes the first. Reported by bosesuneha and the Copilot reviewer. --- .../utils/helper/binaryDownloadHelper.ts | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/src/commands/utils/helper/binaryDownloadHelper.ts b/src/commands/utils/helper/binaryDownloadHelper.ts index 52e3eda05..5c488e277 100644 --- a/src/commands/utils/helper/binaryDownloadHelper.ts +++ b/src/commands/utils/helper/binaryDownloadHelper.ts @@ -65,9 +65,28 @@ function extractFileFromZip(archivePath: string, entryPath: string, destPath: st return; } - let found = false; + // Settle exactly once, always closing the archive first. An open handle + // leaks a file descriptor and, on Windows, blocks the caller from + // deleting the archive afterwards. + let settled = false; + const finish = (error?: Error) => { + if (settled) { + return; + } + settled = true; + try { + zipFile.close(); + } catch { + // Already closed, or never fully opened; nothing to release. + } + if (error) { + reject(error); + } else { + resolve(); + } + }; - zipFile.on("error", reject); + zipFile.on("error", finish); zipFile.on("entry", (entry: yauzl.Entry) => { if (toArchiveEntryPath(entry.fileName) !== wantedEntry) { @@ -75,25 +94,20 @@ function extractFileFromZip(archivePath: string, entryPath: string, destPath: st return; } - found = true; + // First match wins: we stop requesting entries, so a duplicate name + // later in the archive is never read. zipFile.openReadStream(entry, (streamError, readStream) => { if (streamError || !readStream) { - reject(streamError ?? new Error(`Failed to read ${wantedEntry} from archive.`)); + finish(streamError ?? new Error(`Failed to read ${wantedEntry} from archive.`)); return; } - pipeline(readStream, fs.createWriteStream(destPath)).then(() => { - zipFile.close(); - resolve(); - }, reject); + pipeline(readStream, fs.createWriteStream(destPath)).then(() => finish(), finish); }); }); - zipFile.on("end", () => { - if (!found) { - reject(new Error(`Archive does not contain an entry named ${wantedEntry}.`)); - } - }); + // Only reached when no entry matched, since a match stops the iteration. + zipFile.on("end", () => finish(new Error(`Archive does not contain an entry named ${wantedEntry}.`))); zipFile.readEntry(); }); @@ -111,8 +125,11 @@ async function extractFileFromTarball(archivePath: string, entryPath: string, de await tar.list({ file: archivePath, onReadEntry: (entry) => { - if (toArchiveEntryPath(entry.path) !== wantedEntry) { - // Leave it unconsumed; tar drains entries we don't attach to. + // First match wins. A tar is append-only, so the same path may legally + // appear more than once; attaching twice would start two concurrent + // writes to `destPath` and interleave them. Entries we don't attach to + // are drained by tar, so parsing continues either way. + if (writeCompleted || toArchiveEntryPath(entry.path) !== wantedEntry) { return; } @@ -196,8 +213,13 @@ async function downloadTool( error: `Failed to extract ${downloadSpec.pathToBinaryInArchive} from ${downloadFilePath}: ${error}`, }; } finally { - // Remove the archive whether or not extraction succeeded. + // Remove the archive whether or not extraction succeeded, and drop the + // download-once marker along with it. `download.once()` keeps an + // in-memory Completed flag per destination: deleting the file while that + // flag stands would make every retry skip the download and then fail on + // the missing archive, for the lifetime of the window. fs.rmSync(downloadFilePath, { force: true }); + download.clear(downloadFilePath); } } else { await moveFile(downloadFilePath, binaryFilePath);