upload-safety 0.1.0 → 0.2.1
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/README.md +12 -5
- package/dist/heif-metadata.d.ts +14 -0
- package/dist/heif-metadata.d.ts.map +1 -0
- package/dist/heif-metadata.js +216 -0
- package/dist/heif-metadata.js.map +1 -0
- package/dist/photo-metadata.d.ts +5 -4
- package/dist/photo-metadata.d.ts.map +1 -1
- package/dist/photo-metadata.js +84 -4
- package/dist/photo-metadata.js.map +1 -1
- package/dist/text-content.d.ts +7 -0
- package/dist/text-content.d.ts.map +1 -0
- package/dist/text-content.js +83 -0
- package/dist/text-content.js.map +1 -0
- package/dist/upload-allowlist.d.ts +2 -2
- package/dist/upload-allowlist.d.ts.map +1 -1
- package/dist/upload-allowlist.js +27 -1
- package/dist/upload-allowlist.js.map +1 -1
- package/package.json +8 -4
- package/src/heif-metadata.ts +232 -0
- package/src/photo-metadata.ts +88 -4
- package/src/text-content.ts +85 -0
- package/src/upload-allowlist.ts +33 -4
package/src/upload-allowlist.ts
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Strict allowlist + validation for document uploads: PDF, MS Office (also the
|
|
3
|
-
* legacy formats), LibreOffice, and
|
|
3
|
+
* legacy formats), LibreOffice, photos (JPEG, PNG, WebP, HEIC/HEIF, AVIF) and CSV and text. Validation happens on three axes: file
|
|
4
4
|
* extension, declared MIME type, and the actual content, i.e. the magic-byte
|
|
5
5
|
* signature plus the parts a file of that kind always has. Extension and MIME
|
|
6
6
|
* alone are trivially spoofed.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { inspectExcelGlobals } from './excel-macro-sheets.js';
|
|
10
|
+
import { readFtypBrands } from './heif-metadata.js';
|
|
10
11
|
import { readOleDirectory, type OleDirectory } from './ole-streams.js';
|
|
11
12
|
import { inspectPdf } from './pdf-active-content.js';
|
|
12
13
|
import { hasPowerPointCode, isEncryptedPowerPoint } from './powerpoint-vba.js';
|
|
14
|
+
import { inspectText } from './text-content.js';
|
|
13
15
|
import { stripXmlMarkup } from './xml-markup.js';
|
|
14
16
|
import { readZipEntries, readZipEntryData, startsWithBytes, type ZipEntry } from './zip-entries.js';
|
|
15
17
|
|
|
16
18
|
export type AllowedExtension =
|
|
17
|
-
| 'pdf' | 'doc' | 'docx' | 'ppt' | 'pptx' | 'xls' | 'xlsx' | 'odt' | 'ods' | 'odp'
|
|
19
|
+
| 'pdf' | 'doc' | 'docx' | 'ppt' | 'pptx' | 'xls' | 'xlsx' | 'odt' | 'ods' | 'odp'
|
|
20
|
+
| 'jpg' | 'jpeg' | 'png' | 'webp' | 'heic' | 'heif' | 'avif' | 'csv' | 'txt';
|
|
18
21
|
|
|
19
|
-
/** Magic-byte family an extension's bytes must match. */
|
|
20
|
-
type MagicFamily = 'pdf' | 'zip' | 'ole' | 'jpeg' | 'png';
|
|
22
|
+
/** Magic-byte family an extension's bytes must match; text has no signature and is checked on its own. */
|
|
23
|
+
type MagicFamily = 'pdf' | 'zip' | 'ole' | 'jpeg' | 'png' | 'webp' | 'isobmff' | 'text';
|
|
21
24
|
|
|
22
25
|
interface AllowedType {
|
|
23
26
|
extension: AllowedExtension;
|
|
@@ -34,6 +37,8 @@ interface AllowedType {
|
|
|
34
37
|
openDocumentType?: string;
|
|
35
38
|
/** Stream names of a legacy compound file, any one of which must be right below its root. */
|
|
36
39
|
oleStreams?: string[];
|
|
40
|
+
/** `ftyp` brands of an ISOBMFF file, any one of which it must name as major or compatible brand. */
|
|
41
|
+
brands?: string[];
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
const ooxml = ( mainPart: string ) => [ '[content_types].xml', mainPart ];
|
|
@@ -347,6 +352,8 @@ const ODT = 'application/vnd.oasis.opendocument.text';
|
|
|
347
352
|
const ODS = 'application/vnd.oasis.opendocument.spreadsheet';
|
|
348
353
|
const ODP = 'application/vnd.oasis.opendocument.presentation';
|
|
349
354
|
const JPEG = { magic: 'jpeg', mimeTypes: [ 'image/jpeg', 'image/pjpeg' ] } as const satisfies Omit<AllowedType, 'extension'>;
|
|
355
|
+
// HEIC is HEIF with HEVC images; `mif1` names HEIF without saying which codec. Sequences (`msf1`, `hevc`, `hevx`, `avis`) are left out.
|
|
356
|
+
const HEVC_BRANDS = [ 'heic', 'heix', 'heim', 'heis' ];
|
|
350
357
|
|
|
351
358
|
// OOXML (docx/xlsx/pptx) and OpenDocument (odt/ods/odp) files are ZIP
|
|
352
359
|
// containers; legacy Office formats (doc/xls/ppt) are OLE compound files.
|
|
@@ -391,6 +398,15 @@ const ALLOWED_TYPES: readonly AllowedType[] = [
|
|
|
391
398
|
{ extension: 'jpg', ...JPEG },
|
|
392
399
|
{ extension: 'jpeg', ...JPEG },
|
|
393
400
|
{ extension: 'png', magic: 'png', mimeTypes: [ 'image/png' ] },
|
|
401
|
+
{ extension: 'webp', magic: 'webp', mimeTypes: [ 'image/webp' ] },
|
|
402
|
+
{ extension: 'heic', magic: 'isobmff', mimeTypes: [ 'image/heic' ], brands: HEVC_BRANDS },
|
|
403
|
+
{ extension: 'heif', magic: 'isobmff', mimeTypes: [ 'image/heif', 'image/heic' ], brands: [ ...HEVC_BRANDS, 'mif1' ] },
|
|
404
|
+
{ extension: 'avif', magic: 'isobmff', mimeTypes: [ 'image/avif' ], brands: [ 'avif' ] },
|
|
405
|
+
// Windows names the type of a CSV file after Excel.
|
|
406
|
+
{
|
|
407
|
+
extension: 'csv', magic: 'text', mimeTypes: [ 'text/csv', 'application/vnd.ms-excel', 'application/csv', 'text/x-csv', 'text/comma-separated-values', 'text/plain' ],
|
|
408
|
+
},
|
|
409
|
+
{ extension: 'txt', magic: 'text', mimeTypes: [ 'text/plain' ] },
|
|
394
410
|
];
|
|
395
411
|
|
|
396
412
|
const TYPE_BY_EXTENSION = new Map<AllowedExtension, AllowedType>(
|
|
@@ -547,6 +563,12 @@ const MAGIC_SIGNATURES: Record<MagicFamily, ( bytes: Uint8Array ) => boolean> =
|
|
|
547
563
|
jpeg: ( bytes ) => startsWith( bytes, [ 0xff, 0xd8, 0xff ] ),
|
|
548
564
|
// "\x89PNG\r\n\x1A\n"
|
|
549
565
|
png: ( bytes ) => startsWith( bytes, [ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a ] ),
|
|
566
|
+
// "RIFF", the size, "WEBP"
|
|
567
|
+
webp: ( bytes ) => startsWith( bytes, [ 0x52, 0x49, 0x46, 0x46 ] ) && bytes.length >= 12 &&
|
|
568
|
+
startsWith( bytes.subarray( 8 ), [ 0x57, 0x45, 0x42, 0x50 ] ),
|
|
569
|
+
// A box size, then "ftyp"
|
|
570
|
+
isobmff: ( bytes ) => bytes.length >= 8 && startsWith( bytes.subarray( 4 ), [ 0x66, 0x74, 0x79, 0x70 ] ),
|
|
571
|
+
text: () => true,
|
|
550
572
|
};
|
|
551
573
|
|
|
552
574
|
/** What an upload's bytes turned out to be; `macros` and `encrypted` get their own refusal messages. */
|
|
@@ -572,6 +594,13 @@ export function inspectFileContent( extension: AllowedExtension, bytes: Uint8Arr
|
|
|
572
594
|
if ( allowed.oleStreams ) {
|
|
573
595
|
return inspectOleFile( content, allowed.extension, allowed.oleStreams );
|
|
574
596
|
}
|
|
597
|
+
if ( allowed.magic === 'text' ) {
|
|
598
|
+
return inspectText( content, allowed.extension === 'csv' );
|
|
599
|
+
}
|
|
600
|
+
if ( allowed.brands ) {
|
|
601
|
+
const brands = readFtypBrands( content );
|
|
602
|
+
return allowed.brands.some( ( brand ) => brands.includes( brand ) ) ? 'valid' : 'mismatch';
|
|
603
|
+
}
|
|
575
604
|
if ( ! allowed.zipEntries ) {
|
|
576
605
|
return 'valid';
|
|
577
606
|
}
|