diff --git a/packages/zip/README.md b/packages/zip/README.md index 4a1acc563..205ad6070 100644 --- a/packages/zip/README.md +++ b/packages/zip/README.md @@ -26,6 +26,19 @@ let res = await zip.compress('path/to/a/folder', 'path/to/archive.zip', [options let res = await zip.extract('path/to/archive.zip', 'path/to/files', [options]) ``` +`extract` accepts optional uncompressed size limits: + +```js +await zip.extract('path/to/archive.zip', 'path/to/files', { + limits: { + perEntryUncompressedBytes: 536870912, + totalUncompressedBytes: 4294967296, + }, +}); +``` + +Limits default to `Infinity` to preserve existing behaviour when omitted. They are enforced against _actual_ decompressed bytes during streaming, not just the declared central-directory metadata — so a zip whose header lies about its uncompressed size is still caught. + ## Develop This is a mono repository, managed with [Nx](https://nx.dev). diff --git a/packages/zip/lib/extract.js b/packages/zip/lib/extract.js index 2e64ccff5..f2fe2d95c 100644 --- a/packages/zip/lib/extract.js +++ b/packages/zip/lib/extract.js @@ -1,5 +1,11 @@ +const fs = require('fs'); +const path = require('path'); +const { promisify } = require('util'); +const { pipeline, Transform } = require('stream'); const errors = require('@tryghost/errors'); +const pipelineAsync = promisify(pipeline); + const defaultOptions = {}; function normalizeLimit(value, fieldName) { @@ -70,12 +76,8 @@ function getEntryUncompressedSize(entry, limits) { return entry.uncompressedSize; } -function throwOnEntryTooLarge(entry, observedBytes, limitBytes) { - if (observedBytes <= limitBytes) { - return; - } - - throw new errors.UnsupportedMediaTypeError({ +function entryTooLargeError(entry, observedBytes, limitBytes) { + return new errors.UnsupportedMediaTypeError({ message: 'Zip entry exceeds maximum uncompressed size.', context: 'The zip contains an entry that exceeds the maximum uncompressed size.', code: 'ENTRY_TOO_LARGE', @@ -87,12 +89,8 @@ function throwOnEntryTooLarge(entry, observedBytes, limitBytes) { }); } -function throwOnTotalTooLarge(entry, observedBytes, limitBytes, entriesProcessed) { - if (observedBytes <= limitBytes) { - return; - } - - throw new errors.UnsupportedMediaTypeError({ +function totalTooLargeError(entry, observedBytes, limitBytes, entriesProcessed) { + return new errors.UnsupportedMediaTypeError({ message: 'Zip exceeds maximum total uncompressed size.', context: 'The zip exceeds the maximum total uncompressed size.', code: 'TOTAL_TOO_LARGE', @@ -105,6 +103,18 @@ function throwOnTotalTooLarge(entry, observedBytes, limitBytes, entriesProcessed }); } +function throwOnEntryTooLarge(entry, observedBytes, limitBytes) { + if (observedBytes > limitBytes) { + throw entryTooLargeError(entry, observedBytes, limitBytes); + } +} + +function throwOnTotalTooLarge(entry, observedBytes, limitBytes, entriesProcessed) { + if (observedBytes > limitBytes) { + throw totalTooLargeError(entry, observedBytes, limitBytes, entriesProcessed); + } +} + function throwOnSymlinks(entry) { // Check if symlink const mode = (entry.externalFileAttributes >> 16) & 0xffff; @@ -128,6 +138,72 @@ function throwOnLargeFilenames(entry) { } } +// Ported from extract-zip: decide whether an entry is a directory. +function isDirectoryEntry(entry) { + const mode = (entry.externalFileAttributes >> 16) & 0xffff; + const IFMT = 61440; + const IFDIR = 16384; + + if ((mode & IFMT) === IFDIR) { + return true; + } + + if (entry.fileName.endsWith('/')) { + return true; + } + + // Windows-only marker — DOS-mode 16 means "directory" when madeBy is DOS (0). + // See https://github.com/maxogden/extract-zip/issues/13#issuecomment-154494566 + const madeBy = entry.versionMadeBy >> 8; + if (madeBy === 0 && entry.externalFileAttributes === 16) { + return true; + } + + return false; +} + +// Ported from extract-zip: resolve the filesystem mode bits to apply to an entry. +function getEntryMode(entry, opts, isDir) { + let mode = (entry.externalFileAttributes >> 16) & 0xffff; + + if (mode === 0) { + if (isDir) { + mode = opts.defaultDirMode ? parseInt(opts.defaultDirMode, 10) : 0o755; + } else { + mode = opts.defaultFileMode ? parseInt(opts.defaultFileMode, 10) : 0o644; + } + } + + return mode & 0o777; +} + +// Streaming byte counter — enforces per-entry and total limits against actual +// decompressed bytes, not the declared central-directory metadata. This is the +// real zip-bomb defence: a forged uncompressedSize gets past the pre-flight +// metadata check, but the actual decompressed bytes can't slip past this. +class StreamingCounter extends Transform { + constructor({ entry, perEntryLimit, onBytes }) { + super(); + this.entry = entry; + this.perEntryLimit = perEntryLimit; + this.entryBytes = 0; + this.onBytes = onBytes; + } + + _transform(chunk, _encoding, cb) { + this.entryBytes += chunk.length; + if (this.entryBytes > this.perEntryLimit) { + return cb(entryTooLargeError(this.entry, this.entryBytes, this.perEntryLimit)); + } + try { + this.onBytes(chunk.length); + } catch (err) { + return cb(err); + } + cb(null, chunk); + } +} + /** * Extract * @@ -146,37 +222,151 @@ function throwOnLargeFilenames(entry) { module.exports = async (zipToExtract, destination, options) => { const opts = Object.assign({}, defaultOptions, options); const limits = getLimits(opts); - let totalUncompressedBytes = 0; + let totalActualBytes = 0; let entriesProcessed = 0; - const extract = require('extract-zip'); + // Lazy require so tests can swap yauzl out via Module._load before each call. + const yauzl = require('yauzl'); + const openZip = promisify(yauzl.open); - opts.dir = destination; + await fs.promises.mkdir(destination, { recursive: true }); + const dir = await fs.promises.realpath(destination); - const originalOnEntry = opts.onEntry; - opts.onEntry = (entry, zipfile) => { - const entryUncompressedBytes = getEntryUncompressedSize(entry, limits); + // `validateEntrySizes: false` disables yauzl's built-in AssertByteCountStream + // because it overrides the stream's `destroy` in a way that silently swallows + // mid-stream errors and leaves any downstream pipeline waiting forever. We + // enforce sizes ourselves below via StreamingCounter, which propagates errors + // through the pipeline cleanly. + const zipfile = await openZip(zipToExtract, { + lazyEntries: true, + validateEntrySizes: false, + }); - throwOnEntryTooLarge(entry, entryUncompressedBytes, limits.perEntryUncompressedBytes); + let settled = false; - totalUncompressedBytes += entryUncompressedBytes; - entriesProcessed += 1; + try { + await new Promise((resolve, reject) => { + zipfile.on('error', (err) => { + if (settled) { + return; + } + settled = true; + reject(err); + }); - throwOnTotalTooLarge( - entry, - totalUncompressedBytes, - limits.totalUncompressedBytes, - entriesProcessed, - ); + zipfile.on('end', () => { + if (settled) { + return; + } + settled = true; + resolve(); + }); - throwOnSymlinks(entry); - throwOnLargeFilenames(entry); - if (originalOnEntry) { - originalOnEntry(entry, zipfile); - } - }; + zipfile.on('entry', (entry) => { + if (settled) { + return; + } - await extract(zipToExtract, opts); + Promise.resolve() + .then(() => + processEntry(entry, zipfile, opts, limits, dir, { + addTotal: (n) => { + totalActualBytes += n; + throwOnTotalTooLarge( + entry, + totalActualBytes, + limits.totalUncompressedBytes, + entriesProcessed, + ); + }, + markProcessed: () => { + entriesProcessed += 1; + }, + }), + ) + .then(() => { + if (!settled) { + zipfile.readEntry(); + } + }) + .catch((err) => { + if (settled) { + return; + } + settled = true; + reject(err); + }); + }); + + zipfile.readEntry(); + }); + } finally { + try { + zipfile.close(); + } catch { + // best-effort close + } + } return { path: destination }; }; + +async function processEntry(entry, zipfile, opts, limits, dir, { addTotal, markProcessed }) { + // Skip macOS resource-fork entries — they never round-trip cleanly and are noise. + if (entry.fileName.startsWith('__MACOSX/')) { + return; + } + + // Pre-flight metadata check: cheap rejection of zips that honestly declare + // themselves too large, before we touch the read stream. The streaming + // counter below catches archives that lie about their declared size. + const declaredBytes = getEntryUncompressedSize(entry, limits); + throwOnEntryTooLarge(entry, declaredBytes, limits.perEntryUncompressedBytes); + + markProcessed(); + + throwOnSymlinks(entry); + throwOnLargeFilenames(entry); + if (opts.onEntry) { + opts.onEntry(entry, zipfile); + } + + const entryPath = path.join(dir, entry.fileName); + const isDir = isDirectoryEntry(entry); + const mode = getEntryMode(entry, opts, isDir); + const containerDir = isDir ? entryPath : path.dirname(entryPath); + + await fs.promises.mkdir(containerDir, { recursive: true }); + + // Path-traversal guard: after mkdir, resolve the actual on-disk path and + // make sure it stays within `dir`. Catches both `../` shenanigans and + // symlink-target shenanigans created in earlier entries of the same zip. + const canonicalContainerDir = await fs.promises.realpath(containerDir); + const relative = path.relative(dir, canonicalContainerDir); + if (relative.split(path.sep).includes('..')) { + throw new Error( + `Out of bound path "${canonicalContainerDir}" found while processing file ${entry.fileName}`, + ); + } + + if (isDir) { + // Apply requested mode to the directory we just created. + await fs.promises.chmod(entryPath, mode).catch(() => {}); + // Account declared bytes against the total — directories typically declare + // 0, but preserve the pre-existing behaviour of including them in the count. + addTotal(declaredBytes); + return; + } + + const openReadStream = promisify(zipfile.openReadStream.bind(zipfile)); + const readStream = await openReadStream(entry); + const writeStream = fs.createWriteStream(entryPath, { mode }); + + const counter = new StreamingCounter({ + entry, + perEntryLimit: limits.perEntryUncompressedBytes, + onBytes: addTotal, + }); + + await pipelineAsync(readStream, counter, writeStream); +} diff --git a/packages/zip/package.json b/packages/zip/package.json index 49663c142..343e4a13b 100644 --- a/packages/zip/package.json +++ b/packages/zip/package.json @@ -25,7 +25,7 @@ "dependencies": { "@tryghost/errors": "workspace:*", "archiver": "7.0.1", - "extract-zip": "2.0.1" + "yauzl": "3.3.0" }, "devDependencies": { "folder-hash": "4.1.2", diff --git a/packages/zip/test/zip.test.js b/packages/zip/test/zip.test.js index 3fcb19a53..334bc5a48 100644 --- a/packages/zip/test/zip.test.js +++ b/packages/zip/test/zip.test.js @@ -62,6 +62,23 @@ function createZipWithEntries(zipPath, entries) { }); } +// Patches every central-directory file-header's uncompressedSize field (offset +24) +// to make the zip lie about its actual decompressed size. Used to simulate +// zip-bomb-style archives whose declared metadata is smaller than the real payload. +function forgeCentralUncompressedSize(zipPath, fake) { + const buf = fs.readFileSync(zipPath); + const CENTRAL_DIR_SIG = 0x02014b50; + let patched = 0; + for (let i = 0; i < buf.length - 4; i++) { + if (buf.readUInt32LE(i) === CENTRAL_DIR_SIG) { + buf.writeUInt32LE(fake, i + 24); + patched += 1; + } + } + fs.writeFileSync(zipPath, buf); + return patched; +} + describe('Compress and Extract should be opposite functions', function () { let symlinkPath, themeFolder, zipDestination, unzipDestination; @@ -204,38 +221,60 @@ describe('Extract zip', function () { assert.equal(called, true); }); - it('preserves existing behaviour for entries without uncompressed size when no limits are configured', async function () { + function mockYauzlSingleEntry(entry) { const originalLoad = Module._load; Module._load = function (request, parent, isMain) { - if (request === 'extract-zip') { - return async (zipPath, opts) => { - opts.onEntry({ fileName: 'missing-size.txt', externalFileAttributes: 0 }, {}); + if (request === 'yauzl') { + return { + open(zipPath, opts, cb) { + const zipfile = new EventEmitter(); + let entryFired = false; + zipfile.readEntry = () => { + setImmediate(() => { + if (!entryFired) { + entryFired = true; + zipfile.emit('entry', entry); + } else { + zipfile.emit('end'); + } + }); + }; + zipfile.close = () => {}; + cb(null, zipfile); + }, }; } return originalLoad(request, parent, isMain); }; + return () => { + Module._load = originalLoad; + }; + } + + it('preserves existing behaviour for entries without uncompressed size when no limits are configured', async function () { + // Trailing slash marks this entry as a directory, so we don't need to mock openReadStream. + const restore = mockYauzlSingleEntry({ + fileName: 'missing-size-dir/', + externalFileAttributes: 0, + versionMadeBy: 0, + }); try { const result = await extract(zipDestination, unzipDestination); assert.equal(result.path, unzipDestination); } finally { - Module._load = originalLoad; + restore(); } }); it('throws when a limited entry has no uncompressed size', async function () { - const originalLoad = Module._load; - Module._load = function (request, parent, isMain) { - if (request === 'extract-zip') { - return async (zipPath, opts) => { - opts.onEntry({ fileName: 'missing-size.txt', externalFileAttributes: 0 }, {}); - }; - } - - return originalLoad(request, parent, isMain); - }; + const restore = mockYauzlSingleEntry({ + fileName: 'missing-size.txt', + externalFileAttributes: 0, + versionMadeBy: 0, + }); try { await assert.rejects( @@ -261,28 +300,40 @@ describe('Extract zip', function () { }, ); } finally { - Module._load = originalLoad; + restore(); } }); - it('throws when a limited entry has a non-numeric uncompressed size', async function () { - const originalLoad = Module._load; - Module._load = function (request, parent, isMain) { - if (request === 'extract-zip') { - return async (zipPath, opts) => { - opts.onEntry( - { - fileName: 'invalid-size.txt', - externalFileAttributes: 0, - uncompressedSize: Number.NaN, - }, - {}, - ); - }; - } + it('rejects an entry whose path escapes the destination directory', async function () { + // archiver normalizes ../ out of filenames, so we inject the malicious + // entry name via the yauzl mock — this is the same shape an attacker + // could ship in a hand-crafted zip. + const restore = mockYauzlSingleEntry({ + fileName: '../escape.txt', + externalFileAttributes: 0, + versionMadeBy: 0, + }); - return originalLoad(request, parent, isMain); - }; + try { + await assert.rejects( + () => extract(zipDestination, unzipDestination), + (err) => { + assert.match(err.message, /Out of bound path/); + return true; + }, + ); + } finally { + restore(); + } + }); + + it('throws when a limited entry has a non-numeric uncompressed size', async function () { + const restore = mockYauzlSingleEntry({ + fileName: 'invalid-size.txt', + externalFileAttributes: 0, + versionMadeBy: 0, + uncompressedSize: Number.NaN, + }); try { await assert.rejects( @@ -303,7 +354,7 @@ describe('Extract zip', function () { }, ); } finally { - Module._load = originalLoad; + restore(); } }); @@ -414,6 +465,83 @@ describe('Extract zip', function () { assert.equal(fs.existsSync(path.join(unzipDestination, 'second-file.txt')), true); }); + it('rejects a zip whose central directory lies about uncompressed size (zip-bomb defence)', async function () { + // Build a zip with 1MB of content, then forge the central directory to declare 5 bytes. + // Without streaming enforcement, the pre-flight metadata check would pass with a high + // limit, and the underlying decompression would still expand the payload to disk. + const realContent = Buffer.alloc(1024 * 1024, 'a'); + await createZipWithEntries(zipDestination, [{ name: 'lying.txt', content: realContent }]); + const patched = forgeCentralUncompressedSize(zipDestination, 5); + assert.equal(patched >= 1, true, 'expected to patch at least one central directory header'); + + // Pre-flight metadata check sees declared=5, limit=100 — allows through. + // Streaming counter must catch the actual size (1MB > 100) and reject cleanly. + await assert.rejects( + () => + extract(zipDestination, unzipDestination, { + limits: { + perEntryUncompressedBytes: 100, + }, + }), + (err) => { + assert.equal(err.errorType, 'UnsupportedMediaTypeError'); + assert.equal(err.code, 'ENTRY_TOO_LARGE'); + assert.equal(err.errorDetails.entryName, 'lying.txt'); + assert.equal(err.errorDetails.limitBytes, 100); + assert.equal( + err.errorDetails.observedBytes > 100, + true, + `expected observedBytes > 100 (actual streaming bytes), got ${err.errorDetails.observedBytes}`, + ); + return true; + }, + ); + }); + + it('rejects a zip whose cumulative actual bytes exceed the total limit', async function () { + // Two entries: each declares 5 bytes but actually contains 3KB. + // Cumulative actual = 6KB, well over the 100-byte total limit. + const realContent = Buffer.alloc(3 * 1024, 'b'); + await createZipWithEntries(zipDestination, [ + { name: 'first.txt', content: realContent }, + { name: 'second.txt', content: realContent }, + ]); + forgeCentralUncompressedSize(zipDestination, 5); + + await assert.rejects( + () => + extract(zipDestination, unzipDestination, { + limits: { + totalUncompressedBytes: 100, + }, + }), + (err) => { + assert.equal(err.errorType, 'UnsupportedMediaTypeError'); + assert.equal( + err.code === 'TOTAL_TOO_LARGE' || err.code === 'ENTRY_TOO_LARGE', + true, + `expected TOTAL_TOO_LARGE or ENTRY_TOO_LARGE, got ${err.code}`, + ); + return true; + }, + ); + }); + + it('extracts a lying zip when no limits are configured (no streaming enforcement)', async function () { + // With no limits, the streaming counter has nothing to enforce; the underlying + // decompression should write the real bytes and the call should resolve cleanly + // (this is the previously-hanging case under extract-zip + yauzl). + const realContent = Buffer.alloc(1024, 'a'); + await createZipWithEntries(zipDestination, [{ name: 'lying.txt', content: realContent }]); + forgeCentralUncompressedSize(zipDestination, 5); + + await extract(zipDestination, unzipDestination); + + const extracted = path.join(unzipDestination, 'lying.txt'); + assert.equal(fs.existsSync(extracted), true); + assert.equal(fs.statSync(extracted).size, realContent.length); + }); + it('throws when configured size limits are invalid', async function () { await createZipWithEntries(zipDestination, [{ name: 'file.txt', content: '123' }]); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0136a5d9e..1ec888396 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -748,9 +748,9 @@ importers: archiver: specifier: 7.0.1 version: 7.0.1 - extract-zip: - specifier: 2.0.1 - version: 2.0.1 + yauzl: + specifier: 3.3.0 + version: 3.3.0 devDependencies: folder-hash: specifier: 4.1.2 @@ -2579,9 +2579,6 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@types/yauzl@2.10.3': - resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - '@ungap/structured-clone@1.3.1': resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} @@ -3597,11 +3594,6 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} - extract-zip@2.0.1: - resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} - engines: {node: '>= 10.17.0'} - hasBin: true - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -3630,9 +3622,6 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - fd-slicer@1.1.0: - resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -3786,10 +3775,6 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} - get-stream@9.0.1: resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} engines: {node: '>=18'} @@ -5494,8 +5479,9 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} - yauzl@2.10.0: - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yauzl@3.3.0: + resolution: {integrity: sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==} + engines: {node: '>=12'} yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} @@ -7627,11 +7613,6 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@types/yauzl@2.10.3': - dependencies: - '@types/node': 24.12.3 - optional: true - '@ungap/structured-clone@1.3.1': {} '@vitest/coverage-v8@4.1.5(vitest@4.1.5)': @@ -8570,16 +8551,6 @@ snapshots: transitivePeerDependencies: - supports-color - extract-zip@2.0.1: - dependencies: - debug: 4.4.3 - get-stream: 5.2.0 - yauzl: 2.10.0 - optionalDependencies: - '@types/yauzl': 2.10.3 - transitivePeerDependencies: - - supports-color - fast-deep-equal@3.1.3: {} fast-fifo@1.3.2: {} @@ -8610,10 +8581,6 @@ snapshots: dependencies: bser: 2.1.1 - fd-slicer@1.1.0: - dependencies: - pend: 1.2.0 - fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -8747,10 +8714,6 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - get-stream@5.2.0: - dependencies: - pump: 3.0.4 - get-stream@9.0.1: dependencies: '@sec-ant/readable-stream': 0.4.1 @@ -10621,10 +10584,10 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 - yauzl@2.10.0: + yauzl@3.3.0: dependencies: buffer-crc32: 0.2.13 - fd-slicer: 1.1.0 + pend: 1.2.0 yocto-queue@0.1.0: {}