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.
- package/core.js +86 -9
- package/index.js +17 -1
- package/package.json +1 -1
- 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 =
|
|
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 =
|
|
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
|
-
|
|
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, {
|
|
228
|
+
return readZipDataDescriptorEntryWithLimit(zipHandler, {
|
|
229
|
+
shouldBuffer,
|
|
230
|
+
maximumLength: maximumDescriptorLength,
|
|
231
|
+
});
|
|
209
232
|
}
|
|
210
233
|
|
|
211
234
|
if (!shouldBuffer) {
|
|
212
|
-
await zipHandler.tokenizer
|
|
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
|
-
&&
|
|
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,
|
|
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 ?
|
|
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
|
-
|
|
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
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
|