file-type affected by ZIP Decompression Bomb DoS via [Content_Types].xml entry
Description
file-type detects the file type of a file, stream, or data. From 20.0.0 to 21.3.1, a crafted ZIP file can trigger excessive memory growth during type detection in file-type when using fileTypeFromBuffer(), fileTypeFromBlob(), or fileTypeFromFile(). The ZIP inflate output limit is enforced for stream-based detection, but not for known-size inputs. As a result, a small compressed ZIP can cause file-type to inflate and process a much larger payload while probing ZIP-based formats such as OOXML. This vulnerability is fixed in 21.3.2.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CVE-2026-32630: In file-type 20.0.0 to 21.3.1, a crafted ZIP file can cause excessive memory growth during type detection via buffer/blob/file APIs, fixed in 21.3.2.
Vulnerability
Overview
CVE-2026-32630 affects the file-type library (versions 20.0.0 through 21.3.1), which detects file types by examining magic bytes. The vulnerability allows a crafted ZIP file to trigger excessive memory consumption when using the fileTypeFromBuffer(), fileTypeFromBlob(), or fileTypeFromFile() APIs. The root cause is that the ZIP inflate output limit, which is enforced for stream-based detection, is not applied to known-size inputs (buffers, blobs, files). This allows a small compressed ZIP to inflate into a much larger payload during type probing for ZIP-based formats like OOXML [1][3].
Exploitation
An attacker can craft a ZIP file containing a deflated entry with a small compressed size but a very large uncompressed size (a ZIP bomb variant). When file-type processes this file via the affected APIs, it decompresses the entry to probe for known signatures (e.g., OOXML content types). Because the output size limit is missing, the library will allocate memory proportional to the inflated size, leading to excessive memory growth. No authentication or special network position is required; the attack is triggered simply by passing the malicious file to any of the vulnerable detection functions [2][4].
Impact
Impact
Successful exploitation can cause denial of service (DoS) through memory exhaustion, potentially crashing the application or consuming excessive resources. This is particularly dangerous in server-side scenarios where untrusted files are processed (e.g., file upload services). The vulnerability does not lead to code execution or data exfiltration, but the memory impact can be severe [3].
Mitigation
The issue is fixed in version 21.3.2. Users should upgrade immediately. The fix introduces a maximum decompressed size limit for ZIP entries during buffer/blob/file-based detection, mirroring the existing limit for streams [4]. There is no workaround other than upgrading; however, as a general precaution, the library's documentation recommends enforcing file size limits and using worker threads with timeouts when processing untrusted files [1].
AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
file-typenpm | >= 20.0.0, < 21.3.2 | 21.3.2 |
Affected products
2- sindresorhus/file-typev5Range: >= 20.0.0, < 21.3.2
Patches
2a155cd713232Fix ZIP bomb in known-size ZIP probing
2 files changed · +73 −2
core.js+1 −2 modified@@ -237,8 +237,7 @@ ZipHandler.prototype.inflate = async function (zipHeader, fileData, callback) { throw new Error(`Unsupported ZIP compression method: ${zipHeader.compressedMethod}`); } - const maximumLength = hasUnknownFileSize(this.tokenizer) ? maximumZipEntrySizeInBytes : Number.MAX_SAFE_INTEGER; - const uncompressedData = await decompressDeflateRawWithLimit(fileData, {maximumLength}); + const uncompressedData = await decompressDeflateRawWithLimit(fileData, {maximumLength: maximumZipEntrySizeInBytes}); return callback(uncompressedData); };
test.js+72 −0 modified@@ -865,6 +865,24 @@ async function assertZipTypeFromBufferAndChunkedStream(t, bytes) { await assertZipTypeFromChunkedStream(t, bytes); } +async function assertZipTypeFromAllDirectInputs(t, bytes) { + await assertZipTypeFromBuffer(t, bytes); + await assertZipTypeFromBlob(t, bytes); + await assertZipTypeFromFile(t, bytes); + await assertZipTypeFromChunkedStream(t, bytes); +} + +async function assertFileTypeStreamFallsBackToZipWithLargeSampleSize(t, bytes) { + await assertFileTypeStreamNodeResult(t, bytes, { + ext: 'zip', + mime: 'application/zip', + }, {sampleSize: bytes.length}); + await assertFileTypeStreamWebResult(t, bytes, { + ext: 'zip', + mime: 'application/zip', + }, {sampleSize: bytes.length}); +} + async function createTemporaryTestFile(t, bytes, extension = 'zip') { const temporaryDirectory = path.join(__dirname, '.ai-temporary'); await fs.promises.mkdir(temporaryDirectory, {recursive: true}); @@ -976,6 +994,36 @@ function createZipWithLeadingDescriptorContentTypes(descriptorSize) { ]); } +function createZipTextEntryExceedingProbeLimit(text) { + return text + ' '.repeat((maximumZipTextEntrySizeInBytes + 1) - text.length); +} + +function createDeflatedZipWithUnderstatedMimetypeSize() { + const mimetype = createZipTextEntryExceedingProbeLimit('application/epub+zip'); + return createZipLocalFile({ + filename: 'mimetype', + compressedMethod: 8, + compressedData: deflateRawSync(Buffer.from(mimetype)), + uncompressedSize: 1, + }); +} + +function createDeflatedZipWithUnderstatedContentTypesSize() { + const contentTypesXml = createZipTextEntryExceedingProbeLimit(descriptorBoundaryContentTypesXml); + return Buffer.concat([ + createZipLocalFile({ + filename: 'word/document.xml', + compressedData: new TextEncoder().encode('<w:document/>'), + }), + createZipLocalFile({ + filename: '[Content_Types].xml', + compressedMethod: 8, + compressedData: deflateRawSync(Buffer.from(contentTypesXml)), + uncompressedSize: 1, + }), + ]); +} + function createZipArchive(entries) { const localFiles = []; const centralDirectoryEntries = []; @@ -4389,6 +4437,18 @@ test('Falls back to zip for malformed deflated ZIP mimetype entries that oversta await assertZipTypeFromFile(t, malformedZip); }); +test('Falls back to zip for deflated ZIP mimetype entries that understate uncompressed size', async t => { + const mimetypeEntry = createDeflatedZipWithUnderstatedMimetypeSize(); + + await assertZipTypeFromAllDirectInputs(t, mimetypeEntry); +}); + +test('.fileTypeStream() falls back for deflated ZIP mimetype entries that understate uncompressed size with a large sampleSize', async t => { + const mimetypeEntry = createDeflatedZipWithUnderstatedMimetypeSize(); + + await assertFileTypeStreamFallsBackToZipWithLargeSampleSize(t, mimetypeEntry); +}); + test('Does not throw on malformed ZIP with unexpected follow-up signature', async t => { const zipLocalFile = createZipLocalFile({ filename: 'a', @@ -4425,6 +4485,18 @@ test('Falls back to zip for malformed deflated [Content_Types].xml entries that await assertZipTypeFromFile(t, malformedZip); }); +test('Falls back to zip for deflated [Content_Types].xml entries that understate uncompressed size', async t => { + const zip = createDeflatedZipWithUnderstatedContentTypesSize(); + + await assertZipTypeFromAllDirectInputs(t, zip); +}); + +test('.fileTypeStream() falls back for deflated [Content_Types].xml entries that understate uncompressed size with a large sampleSize', async t => { + const zip = createDeflatedZipWithUnderstatedContentTypesSize(); + + await assertFileTypeStreamFallsBackToZipWithLargeSampleSize(t, zip); +}); + test('Does not use directory fallback when malformed deflated oversized [Content_Types].xml appears after a Word entry', async t => { const wordEntry = createZipLocalFile({ filename: 'word/document.xml',
399b0f156063Add support for ZIP decompression using `@tokenizer/inflate` (#695)
2 files changed · +98 −124
core.js+96 −123 modified@@ -4,7 +4,8 @@ Primary entry point, Node.js specific entry point is index.js import * as Token from 'token-types'; import * as strtok3 from 'strtok3/core'; -import {includes, indexOf, getUintBE} from 'uint8array-extras'; +import {ZipHandler} from '@tokenizer/inflate'; +import {includes, getUintBE} from 'uint8array-extras'; import { stringToBytes, tarHeaderChecksumMatches, @@ -26,6 +27,57 @@ export async function fileTypeFromBlob(blob) { return new FileTypeParser().fromBlob(blob); } +function getFileTypeFromMimeType(mimeType) { + switch (mimeType) { + case 'application/epub+zip': + return { + ext: 'epub', + mime: 'application/epub+zip', + }; + case 'application/vnd.oasis.opendocument.text': + return { + ext: 'odt', + mime: 'application/vnd.oasis.opendocument.text', + }; + case 'application/vnd.oasis.opendocument.spreadsheet': + return { + ext: 'ods', + mime: 'application/vnd.oasis.opendocument.spreadsheet', + }; + case 'application/vnd.oasis.opendocument.presentation': + return { + ext: 'odp', + mime: 'application/vnd.oasis.opendocument.presentation', + }; + case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + return { + ext: 'xlsx', + mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }; + case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': + return { + ext: 'docx', + mime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }; + case 'application/vnd.openxmlformats-officedocument.presentationml.presentation': + return { + ext: 'pptx', + mime: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + }; + case 'application/vnd.ms-visio.drawing': + return { + ext: 'vsdx', + mime: 'application/vnd.visio', + }; + case 'application/vnd.ms-package.3dmanufacturing-3dmodel+xml': + return { + ext: '3mf', + mime: 'model/3mf', + }; + default: + } +} + function _check(buffer, headers, options) { options = { offset: 0, @@ -387,140 +439,61 @@ export class FileTypeParser { // Zip-based file formats // Need to be before the `zip` check if (this.check([0x50, 0x4B, 0x3, 0x4])) { // Local file header signature - try { - while (tokenizer.position + 30 < tokenizer.fileInfo.size) { - await tokenizer.readBuffer(this.buffer, {length: 30}); - - const view = new DataView(this.buffer.buffer); - - // https://en.wikipedia.org/wiki/Zip_(file_format)#File_headers - const zipHeader = { - compressedSize: view.getUint32(18, true), - uncompressedSize: view.getUint32(22, true), - filenameLength: view.getUint16(26, true), - extraFieldLength: view.getUint16(28, true), - }; - - zipHeader.filename = await tokenizer.readToken(new Token.StringType(zipHeader.filenameLength, 'utf-8')); - await tokenizer.ignore(zipHeader.extraFieldLength); - - if (/classes\d*\.dex/.test(zipHeader.filename)) { - return { - ext: 'apk', - mime: 'application/vnd.android.package-archive', - }; - } - - // Assumes signed `.xpi` from addons.mozilla.org - if (zipHeader.filename === 'META-INF/mozilla.rsa') { - return { + let fileType; + await new ZipHandler(tokenizer).unzip(zipHeader => { + switch (zipHeader.filename) { + case 'META-INF/mozilla.rsa': + fileType = { ext: 'xpi', mime: 'application/x-xpinstall', }; - } - - if (zipHeader.filename.endsWith('.rels') || zipHeader.filename.endsWith('.xml')) { - const type = zipHeader.filename.split('/')[0]; - switch (type) { - case '_rels': - break; - case 'word': - return { - ext: 'docx', - mime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - }; - case 'ppt': - return { - ext: 'pptx', - mime: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - }; - case 'xl': - return { - ext: 'xlsx', - mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - }; - case 'visio': - return { - ext: 'vsdx', - mime: 'application/vnd.visio', - }; - default: - break; - } - } - - if (zipHeader.filename.startsWith('xl/')) { return { - ext: 'xlsx', - mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + stop: true, }; - } - - if (zipHeader.filename.startsWith('3D/') && zipHeader.filename.endsWith('.model')) { + case 'mimetype': return { - ext: '3mf', - mime: 'model/3mf', + async handler(fileData) { + // Use TextDecoder to decode the UTF-8 encoded data + const mimeType = new TextDecoder('utf-8').decode(fileData).trim(); + fileType = getFileTypeFromMimeType(mimeType); + }, + stop: true, }; - } - // The docx, xlsx and pptx file types extend the Office Open XML file format: - // https://en.wikipedia.org/wiki/Office_Open_XML_file_formats - // We look for: - // - one entry named '[Content_Types].xml' or '_rels/.rels', - // - one entry indicating specific type of file. - // MS Office, OpenOffice and LibreOffice may put the parts in different order, so the check should not rely on it. - if (zipHeader.filename === 'mimetype' && zipHeader.compressedSize === zipHeader.uncompressedSize) { - let mimeType = await tokenizer.readToken(new Token.StringType(zipHeader.compressedSize, 'utf-8')); - mimeType = mimeType.trim(); - - switch (mimeType) { - case 'application/epub+zip': - return { - ext: 'epub', - mime: 'application/epub+zip', - }; - case 'application/vnd.oasis.opendocument.text': - return { - ext: 'odt', - mime: 'application/vnd.oasis.opendocument.text', - }; - case 'application/vnd.oasis.opendocument.spreadsheet': - return { - ext: 'ods', - mime: 'application/vnd.oasis.opendocument.spreadsheet', - }; - case 'application/vnd.oasis.opendocument.presentation': - return { - ext: 'odp', - mime: 'application/vnd.oasis.opendocument.presentation', - }; - default: + case '[Content_Types].xml': + return { + async handler(fileData) { + // Use TextDecoder to decode the UTF-8 encoded data + let xmlContent = new TextDecoder('utf-8').decode(fileData); + const endPos = xmlContent.indexOf('.main+xml"'); + if (endPos >= 0) { + xmlContent = xmlContent.slice(0, Math.max(0, endPos)); + const firstPos = xmlContent.lastIndexOf('"'); + const mimeType = xmlContent.slice(Math.max(0, firstPos + 1)); + fileType = getFileTypeFromMimeType(mimeType); + } else { + const mimeType = 'application/vnd.ms-package.3dmanufacturing-3dmodel+xml'; + if (xmlContent.includes(`ContentType="${mimeType}"`)) { + fileType = getFileTypeFromMimeType(mimeType); + } + } + }, + stop: true, + }; + default: + if (/classes\d*\.dex/.test(zipHeader.filename)) { + fileType = { + ext: 'apk', + mime: 'application/vnd.android.package-archive', + }; + return {stop: true}; } - } - - // Try to find next header manually when current one is corrupted - if (zipHeader.compressedSize === 0) { - let nextHeaderIndex = -1; - - while (nextHeaderIndex < 0 && (tokenizer.position < tokenizer.fileInfo.size)) { - await tokenizer.peekBuffer(this.buffer, {mayBeLess: true}); - nextHeaderIndex = indexOf(this.buffer, new Uint8Array([0x50, 0x4B, 0x03, 0x04])); - - // Move position to the next header if found, skip the whole buffer otherwise - await tokenizer.ignore(nextHeaderIndex >= 0 ? nextHeaderIndex : this.buffer.length); - } - } else { - await tokenizer.ignore(zipHeader.compressedSize); - } + return {}; } - } catch (error) { - if (!(error instanceof strtok3.EndOfStreamError)) { - throw error; - } - } + }); - return { + return fileType ?? { ext: 'zip', mime: 'application/zip', };
package.json+2 −1 modified@@ -222,7 +222,8 @@ "lz4" ], "dependencies": { - "strtok3": "^10.0.0", + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.0.1", "token-types": "^6.0.0", "uint8array-extras": "^1.3.0" },
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-j47w-4g3g-c36vghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-32630ghsaADVISORY
- github.com/sindresorhus/file-type/commit/399b0f156063f5aeb1c124a7fd61028f3ea7c124ghsax_refsource_MISCWEB
- github.com/sindresorhus/file-type/commit/a155cd71323279de173c54e8c530d300d3854fddghsaWEB
- github.com/sindresorhus/file-type/releases/tag/v21.3.2ghsaWEB
- github.com/sindresorhus/file-type/security/advisories/GHSA-j47w-4g3g-c36vghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.