file-type 21.3.2 → 21.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/core.js +86 -9
  2. package/index.js +17 -1
  3. package/package.json +1 -1
  4. package/readme.md +3 -0
package/core.js CHANGED
@@ -20,18 +20,22 @@ const maximumZipEntrySizeInBytes = 1024 * 1024;
20
20
  const maximumZipEntryCount = 1024;
21
21
  const maximumZipBufferedReadSizeInBytes = (2 ** 31) - 1;
22
22
  const maximumUntrustedSkipSizeInBytes = 16 * 1024 * 1024;
23
+ const maximumUnknownSizePayloadProbeSizeInBytes = maximumZipEntrySizeInBytes;
23
24
  const maximumZipTextEntrySizeInBytes = maximumZipEntrySizeInBytes;
24
25
  const maximumNestedGzipDetectionSizeInBytes = maximumUntrustedSkipSizeInBytes;
25
26
  const maximumNestedGzipProbeDepth = 1;
26
27
  const maximumId3HeaderSizeInBytes = maximumUntrustedSkipSizeInBytes;
27
28
  const maximumEbmlDocumentTypeSizeInBytes = 64;
28
- const maximumEbmlElementPayloadSizeInBytes = maximumUntrustedSkipSizeInBytes;
29
+ const maximumEbmlElementPayloadSizeInBytes = maximumUnknownSizePayloadProbeSizeInBytes;
29
30
  const maximumEbmlElementCount = 256;
30
31
  const maximumPngChunkCount = 512;
32
+ const maximumPngStreamScanBudgetInBytes = maximumUntrustedSkipSizeInBytes;
31
33
  const maximumAsfHeaderObjectCount = 512;
32
34
  const maximumTiffTagCount = 512;
33
35
  const maximumDetectionReentryCount = 256;
34
- const maximumPngChunkSizeInBytes = maximumUntrustedSkipSizeInBytes;
36
+ const maximumPngChunkSizeInBytes = maximumUnknownSizePayloadProbeSizeInBytes;
37
+ const maximumAsfHeaderPayloadSizeInBytes = maximumUnknownSizePayloadProbeSizeInBytes;
38
+ const maximumTiffStreamIfdOffsetInBytes = maximumUnknownSizePayloadProbeSizeInBytes;
35
39
  const maximumTiffIfdOffsetInBytes = maximumUntrustedSkipSizeInBytes;
36
40
  const recoverableZipErrorMessages = new Set([
37
41
  'Unexpected signature',
@@ -141,6 +145,10 @@ function findZipDataDescriptorOffset(buffer, bytesConsumed) {
141
145
  return -1;
142
146
  }
143
147
 
148
+ function isPngAncillaryChunk(type) {
149
+ return (type.codePointAt(0) & 0x20) !== 0;
150
+ }
151
+
144
152
  function mergeByteChunks(chunks, totalLength) {
145
153
  const merged = new Uint8Array(totalLength);
146
154
  let offset = 0;
@@ -193,6 +201,10 @@ async function readZipDataDescriptorEntryWithLimit(zipHandler, {shouldBuffer, ma
193
201
  }
194
202
  }
195
203
 
204
+ if (!hasUnknownFileSize(zipHandler.tokenizer)) {
205
+ zipHandler.knownSizeDescriptorScannedBytes += bytesConsumed;
206
+ }
207
+
196
208
  if (!shouldBuffer) {
197
209
  return;
198
210
  }
@@ -200,16 +212,30 @@ async function readZipDataDescriptorEntryWithLimit(zipHandler, {shouldBuffer, ma
200
212
  return mergeByteChunks(chunks, bytesConsumed);
201
213
  }
202
214
 
203
- async function readZipEntryData(zipHandler, zipHeader, {shouldBuffer} = {}) {
215
+ function getRemainingZipScanBudget(zipHandler, startOffset) {
216
+ if (hasUnknownFileSize(zipHandler.tokenizer)) {
217
+ return Math.max(0, maximumUntrustedSkipSizeInBytes - (zipHandler.tokenizer.position - startOffset));
218
+ }
219
+
220
+ return Math.max(0, maximumZipEntrySizeInBytes - zipHandler.knownSizeDescriptorScannedBytes);
221
+ }
222
+
223
+ async function readZipEntryData(zipHandler, zipHeader, {shouldBuffer, maximumDescriptorLength = maximumZipEntrySizeInBytes} = {}) {
204
224
  if (
205
225
  zipHeader.dataDescriptor
206
226
  && zipHeader.compressedSize === 0
207
227
  ) {
208
- return readZipDataDescriptorEntryWithLimit(zipHandler, {shouldBuffer});
228
+ return readZipDataDescriptorEntryWithLimit(zipHandler, {
229
+ shouldBuffer,
230
+ maximumLength: maximumDescriptorLength,
231
+ });
209
232
  }
210
233
 
211
234
  if (!shouldBuffer) {
212
- await zipHandler.tokenizer.ignore(zipHeader.compressedSize);
235
+ await safeIgnore(zipHandler.tokenizer, zipHeader.compressedSize, {
236
+ maximumLength: hasUnknownFileSize(zipHandler.tokenizer) ? maximumZipEntrySizeInBytes : zipHandler.tokenizer.fileInfo.size,
237
+ reason: 'ZIP entry compressed data',
238
+ });
213
239
  return;
214
240
  }
215
241
 
@@ -244,7 +270,13 @@ ZipHandler.prototype.inflate = async function (zipHeader, fileData, callback) {
244
270
  ZipHandler.prototype.unzip = async function (fileCallback) {
245
271
  let stop = false;
246
272
  let zipEntryCount = 0;
273
+ const zipScanStart = this.tokenizer.position;
274
+ this.knownSizeDescriptorScannedBytes = 0;
247
275
  do {
276
+ if (hasExceededUnknownSizeScanBudget(this.tokenizer, zipScanStart, maximumUntrustedSkipSizeInBytes)) {
277
+ throw new ParserHardLimitError(`ZIP stream probing exceeds ${maximumUntrustedSkipSizeInBytes} bytes`);
278
+ }
279
+
248
280
  const zipHeader = await this.readLocalFileHeader();
249
281
  if (!zipHeader) {
250
282
  break;
@@ -260,6 +292,7 @@ ZipHandler.prototype.unzip = async function (fileCallback) {
260
292
  await this.tokenizer.ignore(zipHeader.extraFieldLength);
261
293
  const fileData = await readZipEntryData(this, zipHeader, {
262
294
  shouldBuffer: Boolean(next.handler),
295
+ maximumDescriptorLength: Math.min(maximumZipEntrySizeInBytes, getRemainingZipScanBudget(this, zipScanStart)),
263
296
  });
264
297
 
265
298
  if (next.handler) {
@@ -273,6 +306,10 @@ ZipHandler.prototype.unzip = async function (fileCallback) {
273
306
  throw new Error(`Expected data-descriptor-signature at position ${this.tokenizer.position - dataDescriptor.length}`);
274
307
  }
275
308
  }
309
+
310
+ if (hasExceededUnknownSizeScanBudget(this.tokenizer, zipScanStart, maximumUntrustedSkipSizeInBytes)) {
311
+ throw new ParserHardLimitError(`ZIP stream probing exceeds ${maximumUntrustedSkipSizeInBytes} bytes`);
312
+ }
276
313
  } while (!stop);
277
314
  };
278
315
 
@@ -1001,7 +1038,10 @@ export class FileTypeParser {
1001
1038
  // Keep ID3 probing bounded for unknown-size streams to avoid attacker-controlled large skips.
1002
1039
  || (
1003
1040
  isUnknownFileSize
1004
- && id3HeaderLength > maximumId3HeaderSizeInBytes
1041
+ && (
1042
+ id3HeaderLength > maximumId3HeaderSizeInBytes
1043
+ || (tokenizer.position + id3HeaderLength) > maximumId3HeaderSizeInBytes
1044
+ )
1005
1045
  )
1006
1046
  ) {
1007
1047
  return;
@@ -1454,6 +1494,10 @@ export class FileTypeParser {
1454
1494
  return;
1455
1495
  }
1456
1496
 
1497
+ if (hasExceededUnknownSizeScanBudget(tokenizer, ebmlScanStart, maximumUntrustedSkipSizeInBytes)) {
1498
+ return;
1499
+ }
1500
+
1457
1501
  const previousPosition = tokenizer.position;
1458
1502
  const element = await readElement();
1459
1503
 
@@ -1493,6 +1537,7 @@ export class FileTypeParser {
1493
1537
  }
1494
1538
 
1495
1539
  const rootElement = await readElement();
1540
+ const ebmlScanStart = tokenizer.position;
1496
1541
  const documentType = await readChildren(rootElement.len);
1497
1542
 
1498
1543
  switch (documentType) {
@@ -1875,13 +1920,14 @@ export class FileTypeParser {
1875
1920
  const isUnknownPngStream = hasUnknownFileSize(tokenizer);
1876
1921
  const pngScanStart = tokenizer.position;
1877
1922
  let pngChunkCount = 0;
1923
+ let hasSeenImageHeader = false;
1878
1924
  do {
1879
1925
  pngChunkCount++;
1880
1926
  if (pngChunkCount > maximumPngChunkCount) {
1881
1927
  break;
1882
1928
  }
1883
1929
 
1884
- if (hasExceededUnknownSizeScanBudget(tokenizer, pngScanStart, maximumPngChunkSizeInBytes)) {
1930
+ if (hasExceededUnknownSizeScanBudget(tokenizer, pngScanStart, maximumPngStreamScanBudgetInBytes)) {
1885
1931
  break;
1886
1932
  }
1887
1933
 
@@ -1891,18 +1937,34 @@ export class FileTypeParser {
1891
1937
  return; // Invalid chunk length
1892
1938
  }
1893
1939
 
1940
+ if (chunk.type === 'IHDR') {
1941
+ // PNG requires the first real image header to be a 13-byte IHDR chunk.
1942
+ if (chunk.length !== 13) {
1943
+ return;
1944
+ }
1945
+
1946
+ hasSeenImageHeader = true;
1947
+ }
1948
+
1894
1949
  switch (chunk.type) {
1895
1950
  case 'IDAT':
1896
1951
  return pngFileType;
1897
1952
  case 'acTL':
1898
1953
  return apngFileType;
1899
1954
  default:
1955
+ if (
1956
+ !hasSeenImageHeader
1957
+ && chunk.type !== 'CgBI'
1958
+ ) {
1959
+ return;
1960
+ }
1961
+
1900
1962
  if (
1901
1963
  isUnknownPngStream
1902
1964
  && chunk.length > maximumPngChunkSizeInBytes
1903
1965
  ) {
1904
1966
  // Avoid huge attacker-controlled skips when probing unknown-size streams.
1905
- return;
1967
+ return hasSeenImageHeader && isPngAncillaryChunk(chunk.type) ? pngFileType : undefined;
1906
1968
  }
1907
1969
 
1908
1970
  try {
@@ -2158,8 +2220,16 @@ export class FileTypeParser {
2158
2220
  break;
2159
2221
  }
2160
2222
 
2223
+ if (
2224
+ isUnknownFileSize
2225
+ && payload > maximumAsfHeaderPayloadSizeInBytes
2226
+ ) {
2227
+ isMalformedAsf = true;
2228
+ break;
2229
+ }
2230
+
2161
2231
  await safeIgnore(tokenizer, payload, {
2162
- maximumLength: isUnknownFileSize ? maximumUntrustedSkipSizeInBytes : tokenizer.fileInfo.size,
2232
+ maximumLength: isUnknownFileSize ? maximumAsfHeaderPayloadSizeInBytes : tokenizer.fileInfo.size,
2163
2233
  reason: 'ASF header payload',
2164
2234
  });
2165
2235
 
@@ -2625,6 +2695,13 @@ export class FileTypeParser {
2625
2695
  }
2626
2696
  }
2627
2697
 
2698
+ if (
2699
+ hasUnknownFileSize(this.tokenizer)
2700
+ && ifdOffset > maximumTiffStreamIfdOffsetInBytes
2701
+ ) {
2702
+ return tiffFileType;
2703
+ }
2704
+
2628
2705
  const maximumTiffOffset = hasUnknownFileSize(this.tokenizer) ? maximumTiffIfdOffsetInBytes : this.tokenizer.fileInfo.size;
2629
2706
 
2630
2707
  try {
package/index.js CHANGED
@@ -4,6 +4,8 @@ Node.js specific entry point.
4
4
 
5
5
  import {ReadableStream as WebReadableStream} from 'node:stream/web';
6
6
  import {pipeline, PassThrough, Readable} from 'node:stream';
7
+ import fs from 'node:fs/promises';
8
+ import {constants as fileSystemConstants} from 'node:fs';
7
9
  import * as strtok3 from 'strtok3';
8
10
  import {
9
11
  FileTypeParser as DefaultFileTypeParser,
@@ -42,7 +44,21 @@ export class FileTypeParser extends DefaultFileTypeParser {
42
44
  }
43
45
 
44
46
  async fromFile(path) {
45
- const tokenizer = await strtok3.fromFile(path);
47
+ // TODO: Remove this when `strtok3.fromFile()` safely rejects non-regular filesystem objects without a pathname race.
48
+ const fileHandle = await fs.open(path, fileSystemConstants.O_RDONLY | fileSystemConstants.O_NONBLOCK);
49
+ const fileStat = await fileHandle.stat();
50
+ if (!fileStat.isFile()) {
51
+ await fileHandle.close();
52
+ return;
53
+ }
54
+
55
+ const tokenizer = new strtok3.FileTokenizer(fileHandle, {
56
+ ...this.getTokenizerOptions(),
57
+ fileInfo: {
58
+ path,
59
+ size: fileStat.size,
60
+ },
61
+ });
46
62
  try {
47
63
  return await super.fromTokenizer(tokenizer);
48
64
  } finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "file-type",
3
- "version": "21.3.2",
3
+ "version": "21.3.3",
4
4
  "description": "Detect the file type of a file, stream, or data",
5
5
  "license": "MIT",
6
6
  "repository": "sindresorhus/file-type",
package/readme.md CHANGED
@@ -10,6 +10,9 @@ This package is for detecting binary-based file formats, not text-based formats
10
10
 
11
11
  We accept contributions for commonly used modern file formats, not historical or obscure ones. Open an issue first for discussion.
12
12
 
13
+ > [!IMPORTANT]
14
+ > NO SECURITY REPORTS WILL BE ACCEPTED RIGHT NOW. I'm currently hardening the parser and all the low-quality AI-generated security reports is just a huge waste of time.
15
+
13
16
  ## Install
14
17
 
15
18
  ```sh