@blamejs/core 0.7.4 → 0.7.18
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/CHANGELOG.md +423 -395
- package/README.md +150 -149
- package/bin/blamejs.js +0 -0
- package/index.js +308 -284
- package/lib/api-key.js +660 -663
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt.js +311 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +594 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/codepoint-class.js +196 -0
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -286
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -421
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/file-upload.js +213 -10
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -215
- package/lib/framework-schema.js +717 -717
- package/lib/gate-contract.js +971 -0
- package/lib/guard-all.js +405 -0
- package/lib/guard-archive.js +739 -0
- package/lib/guard-csv.js +816 -0
- package/lib/guard-email.js +744 -0
- package/lib/guard-filename.js +724 -0
- package/lib/guard-html.js +976 -0
- package/lib/guard-json.js +729 -0
- package/lib/guard-markdown.js +586 -0
- package/lib/guard-svg.js +976 -0
- package/lib/guard-xml.js +405 -0
- package/lib/guard-yaml.js +529 -0
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -362
- package/lib/mail.js +981 -962
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/body-parser.js +1170 -1091
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +79 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -91
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +478 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +184 -4
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -319
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
package/lib/file-type.js
CHANGED
|
@@ -1,273 +1,273 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* file-type — magic-byte content detection.
|
|
4
|
-
*
|
|
5
|
-
* MIME on a multipart upload comes from the CLIENT — a malicious
|
|
6
|
-
* uploader can label a polyglot HTML payload as "image/png" and the
|
|
7
|
-
* Content-Type header alone won't catch it. This primitive inspects
|
|
8
|
-
* the leading bytes of a buffer against a hardcoded magic-byte
|
|
9
|
-
* registry and returns the actual format independently of the
|
|
10
|
-
* advertised MIME.
|
|
11
|
-
*
|
|
12
|
-
* var detected = b.fileType.detect(buffer);
|
|
13
|
-
* // → { mime: "image/png", extension: "png", category: "image" }
|
|
14
|
-
* // OR null when no signature matches
|
|
15
|
-
*
|
|
16
|
-
* var ok = b.fileType.assertOneOf(buffer, ["image/png", "image/jpeg", "application/pdf"]);
|
|
17
|
-
* // → throws FileTypeError if the actual format isn't in the allowlist
|
|
18
|
-
*
|
|
19
|
-
* Coverage targets the formats most likely to flow through a typical
|
|
20
|
-
* web app's upload boundary: images (PNG/JPEG/GIF/WEBP/AVIF/HEIC),
|
|
21
|
-
* documents (PDF/DOCX/XLSX/PPTX), archives (ZIP/RAR/7Z/TAR/GZ),
|
|
22
|
-
* audio/video (MP3/MP4/WEBM), and a small set of executable formats
|
|
23
|
-
* to deny on upload (PE/ELF/Mach-O). Operators with format coverage
|
|
24
|
-
* outside this list either pass an `extra` registry to extend, or
|
|
25
|
-
* fall back to libmagic via an external sandbox process.
|
|
26
|
-
*
|
|
27
|
-
* Validation policy:
|
|
28
|
-
* - detect(buffer) → returns null on bad input rather than throwing
|
|
29
|
-
* (saved-for-later analysis often runs against partial reads)
|
|
30
|
-
* - assertOneOf(buffer, allowlist[, opts]) throws on mismatch.
|
|
31
|
-
* Operator opt: `allowEmpty: true` to permit zero-length buffers
|
|
32
|
-
* (default false — empty multipart parts are usually a mistake).
|
|
33
|
-
*
|
|
34
|
-
* Out of scope (operator brings their own):
|
|
35
|
-
* - Content disarm (CDR — strip Office macros, PDF JS, etc.).
|
|
36
|
-
* CDR is genuinely hard and format-specific; operators with that
|
|
37
|
-
* requirement reach for a sandbox like dangerzone or vmray.
|
|
38
|
-
* - Polyglot file detection (a single file that is BOTH valid PDF
|
|
39
|
-
* AND valid HTML). detect() returns the first signature match;
|
|
40
|
-
* true polyglot defense needs structural validation per format.
|
|
41
|
-
* - Filename-extension validation. The framework's view is:
|
|
42
|
-
* extensions are operator-controlled metadata, magic bytes are
|
|
43
|
-
* the wire truth.
|
|
44
|
-
*/
|
|
45
|
-
var C = require("./constants");
|
|
46
|
-
var { defineClass } = require("./framework-error");
|
|
47
|
-
|
|
48
|
-
var FileTypeError = defineClass("FileTypeError", { alwaysPermanent: true });
|
|
49
|
-
var _err = FileTypeError.factory;
|
|
50
|
-
|
|
51
|
-
// Cap on bytes inspected for OOXML / WEBP shape-check probes. Set well
|
|
52
|
-
// above any legitimate Office central-directory prefix and any RIFF
|
|
53
|
-
// header — operators don't tune this, the magic-byte registry is fixed.
|
|
54
|
-
var SNIFF_HEAD_BYTES = C.BYTES.kib(4);
|
|
55
|
-
|
|
56
|
-
// Signature registry. Each entry: { name, mime, extension, category,
|
|
57
|
-
// offset, magic: Buffer | [Buffer, ...], extra?: function(buffer)→bool }.
|
|
58
|
-
// Order matters — earlier entries win on ambiguous matches (e.g. ZIP
|
|
59
|
-
// shape catches OOXML, so OOXML-specific entries come FIRST).
|
|
60
|
-
var SIGNATURES = [
|
|
61
|
-
// ---- Office Open XML (DOCX/XLSX/PPTX) ----
|
|
62
|
-
// OOXML files are ZIP archives whose central directory contains
|
|
63
|
-
// [Content_Types].xml. The cheap shape-check is the ZIP local-file
|
|
64
|
-
// header (0x50 0x4B 0x03 0x04) PLUS the filename "[Content_Types].xml"
|
|
65
|
-
// appearing within the first 256 bytes — present for every
|
|
66
|
-
// well-formed OOXML produced by Office / LibreOffice / etc.
|
|
67
|
-
{ name: "docx", mime: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
68
|
-
extension: "docx", category: "document",
|
|
69
|
-
offset: 0, magic: Buffer.from([0x50, 0x4B, 0x03, 0x04]),
|
|
70
|
-
extra: function (buf) {
|
|
71
|
-
var head = buf.subarray(0, Math.min(buf.length, SNIFF_HEAD_BYTES)).toString("binary");
|
|
72
|
-
return head.indexOf("word/") !== -1 || head.indexOf("[Content_Types].xml") !== -1 && head.indexOf("word") !== -1;
|
|
73
|
-
} },
|
|
74
|
-
{ name: "xlsx", mime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
75
|
-
extension: "xlsx", category: "document",
|
|
76
|
-
offset: 0, magic: Buffer.from([0x50, 0x4B, 0x03, 0x04]),
|
|
77
|
-
extra: function (buf) {
|
|
78
|
-
var head = buf.subarray(0, Math.min(buf.length, SNIFF_HEAD_BYTES)).toString("binary");
|
|
79
|
-
return head.indexOf("xl/") !== -1;
|
|
80
|
-
} },
|
|
81
|
-
{ name: "pptx", mime: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
82
|
-
extension: "pptx", category: "document",
|
|
83
|
-
offset: 0, magic: Buffer.from([0x50, 0x4B, 0x03, 0x04]),
|
|
84
|
-
extra: function (buf) {
|
|
85
|
-
var head = buf.subarray(0, Math.min(buf.length, SNIFF_HEAD_BYTES)).toString("binary");
|
|
86
|
-
return head.indexOf("ppt/") !== -1;
|
|
87
|
-
} },
|
|
88
|
-
// ---- Plain ZIP (after OOXML so OOXML wins) ----
|
|
89
|
-
{ name: "zip", mime: "application/zip", extension: "zip", category: "archive",
|
|
90
|
-
offset: 0, magic: [
|
|
91
|
-
Buffer.from([0x50, 0x4B, 0x03, 0x04]), // standard local file header
|
|
92
|
-
Buffer.from([0x50, 0x4B, 0x05, 0x06]), // empty archive
|
|
93
|
-
Buffer.from([0x50, 0x4B, 0x07, 0x08]), // spanned archive
|
|
94
|
-
] },
|
|
95
|
-
|
|
96
|
-
// ---- Images ----
|
|
97
|
-
{ name: "png", mime: "image/png", extension: "png", category: "image",
|
|
98
|
-
offset: 0, magic: Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) },
|
|
99
|
-
{ name: "jpeg", mime: "image/jpeg", extension: "jpg", category: "image",
|
|
100
|
-
offset: 0, magic: Buffer.from([0xFF, 0xD8, 0xFF]) },
|
|
101
|
-
{ name: "gif", mime: "image/gif", extension: "gif", category: "image",
|
|
102
|
-
offset: 0, magic: [Buffer.from("GIF87a", "ascii"), Buffer.from("GIF89a", "ascii")] },
|
|
103
|
-
{ name: "webp", mime: "image/webp", extension: "webp", category: "image",
|
|
104
|
-
offset: 0, magic: Buffer.from("RIFF", "ascii"),
|
|
105
|
-
extra: function (buf) {
|
|
106
|
-
// RIFF header: "RIFF" (0..3), file size (4..7), format tag at offset 8..11.
|
|
107
|
-
return buf.length >= C.BYTES.bytes(12) &&
|
|
108
|
-
buf.subarray(C.BYTES.bytes(0x08), C.BYTES.bytes(12)).toString("ascii") === "WEBP";
|
|
109
|
-
} },
|
|
110
|
-
{ name: "bmp", mime: "image/bmp", extension: "bmp", category: "image",
|
|
111
|
-
offset: 0, magic: Buffer.from([0x42, 0x4D]) },
|
|
112
|
-
{ name: "tiff", mime: "image/tiff", extension: "tiff", category: "image",
|
|
113
|
-
offset: 0, magic: [
|
|
114
|
-
Buffer.from([0x49, 0x49, 0x2A, 0x00]), // little-endian
|
|
115
|
-
Buffer.from([0x4D, 0x4D, 0x00, 0x2A]), // big-endian
|
|
116
|
-
] },
|
|
117
|
-
{ name: "avif", mime: "image/avif", extension: "avif", category: "image",
|
|
118
|
-
offset: 4, magic: Buffer.from("ftypavif", "ascii") },
|
|
119
|
-
{ name: "heic", mime: "image/heic", extension: "heic", category: "image",
|
|
120
|
-
offset: 4, magic: [
|
|
121
|
-
Buffer.from("ftypheic", "ascii"),
|
|
122
|
-
Buffer.from("ftypheix", "ascii"),
|
|
123
|
-
Buffer.from("ftypmif1", "ascii"),
|
|
124
|
-
Buffer.from("ftypmsf1", "ascii"),
|
|
125
|
-
] },
|
|
126
|
-
|
|
127
|
-
// ---- Documents (non-OOXML) ----
|
|
128
|
-
{ name: "pdf", mime: "application/pdf", extension: "pdf", category: "document",
|
|
129
|
-
offset: 0, magic: Buffer.from("%PDF-", "ascii") },
|
|
130
|
-
{ name: "rtf", mime: "application/rtf", extension: "rtf", category: "document",
|
|
131
|
-
offset: 0, magic: Buffer.from("{\\rtf", "ascii") },
|
|
132
|
-
// CFB (Compound File Binary) — old Office (.doc/.xls/.ppt before 2007),
|
|
133
|
-
// also MSI installers.
|
|
134
|
-
{ name: "cfb", mime: "application/x-cfb", extension: "doc", category: "document",
|
|
135
|
-
offset: 0, magic: Buffer.from([0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]) },
|
|
136
|
-
|
|
137
|
-
// ---- Archives ----
|
|
138
|
-
{ name: "rar", mime: "application/vnd.rar", extension: "rar", category: "archive",
|
|
139
|
-
offset: 0, magic: [
|
|
140
|
-
Buffer.from([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00]), // RAR 1.5
|
|
141
|
-
Buffer.from([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00]), // RAR 5
|
|
142
|
-
] },
|
|
143
|
-
{ name: "7z", mime: "application/x-7z-compressed", extension: "7z", category: "archive",
|
|
144
|
-
offset: 0, magic: Buffer.from([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]) },
|
|
145
|
-
{ name: "gz", mime: "application/gzip", extension: "gz", category: "archive",
|
|
146
|
-
offset: 0, magic: Buffer.from([0x1F, 0x8B]) },
|
|
147
|
-
{ name: "bz2", mime: "application/x-bzip2", extension: "bz2", category: "archive",
|
|
148
|
-
offset: 0, magic: Buffer.from("BZh", "ascii") },
|
|
149
|
-
{ name: "xz", mime: "application/x-xz", extension: "xz", category: "archive",
|
|
150
|
-
offset: 0, magic: Buffer.from([0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]) },
|
|
151
|
-
{ name: "tar", mime: "application/x-tar", extension: "tar", category: "archive",
|
|
152
|
-
offset: 257, magic: Buffer.from("ustar", "ascii") },
|
|
153
|
-
|
|
154
|
-
// ---- Audio / video ----
|
|
155
|
-
{ name: "mp3", mime: "audio/mpeg", extension: "mp3", category: "audio",
|
|
156
|
-
offset: 0, magic: [
|
|
157
|
-
Buffer.from([0x49, 0x44, 0x33]), // ID3v2 tag
|
|
158
|
-
Buffer.from([0xFF, 0xFB]), // MPEG audio frame (no ID3)
|
|
159
|
-
Buffer.from([0xFF, 0xF3]),
|
|
160
|
-
Buffer.from([0xFF, 0xF2]),
|
|
161
|
-
] },
|
|
162
|
-
{ name: "mp4", mime: "video/mp4", extension: "mp4", category: "video",
|
|
163
|
-
offset: 4, magic: [
|
|
164
|
-
Buffer.from("ftypisom", "ascii"),
|
|
165
|
-
Buffer.from("ftypiso2", "ascii"),
|
|
166
|
-
Buffer.from("ftypmp42", "ascii"),
|
|
167
|
-
Buffer.from("ftypM4V ", "ascii"),
|
|
168
|
-
] },
|
|
169
|
-
{ name: "webm", mime: "video/webm", extension: "webm", category: "video",
|
|
170
|
-
offset: 0, magic: Buffer.from([0x1A, 0x45, 0xDF, 0xA3]) },
|
|
171
|
-
|
|
172
|
-
// ---- Executables (operators usually DENY these on upload) ----
|
|
173
|
-
{ name: "pe", mime: "application/x-msdownload", extension: "exe", category: "executable",
|
|
174
|
-
offset: 0, magic: Buffer.from([0x4D, 0x5A]) },
|
|
175
|
-
{ name: "elf", mime: "application/x-executable", extension: "elf", category: "executable",
|
|
176
|
-
offset: 0, magic: Buffer.from([0x7F, 0x45, 0x4C, 0x46]) },
|
|
177
|
-
{ name: "macho", mime: "application/x-mach-binary", extension: "macho", category: "executable",
|
|
178
|
-
offset: 0, magic: [
|
|
179
|
-
Buffer.from([0xFE, 0xED, 0xFA, 0xCE]), // big-endian, narrow word
|
|
180
|
-
Buffer.from([0xFE, 0xED, 0xFA, 0xCF]), // big-endian, wide word
|
|
181
|
-
Buffer.from([0xCE, 0xFA, 0xED, 0xFE]), // little-endian, narrow word
|
|
182
|
-
Buffer.from([0xCF, 0xFA, 0xED, 0xFE]), // little-endian, wide word
|
|
183
|
-
Buffer.from([0xCA, 0xFE, 0xBA, 0xBE]), // universal binary (also Java .class)
|
|
184
|
-
] },
|
|
185
|
-
];
|
|
186
|
-
|
|
187
|
-
function _matchesAt(buf, offset, magic) {
|
|
188
|
-
if (buf.length < offset + magic.length) return false;
|
|
189
|
-
for (var i = 0; i < magic.length; i++) {
|
|
190
|
-
if (buf[offset + i] !== magic[i]) return false;
|
|
191
|
-
}
|
|
192
|
-
return true;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function _entryMatches(entry, buf) {
|
|
196
|
-
var magics = Array.isArray(entry.magic) ? entry.magic : [entry.magic];
|
|
197
|
-
var matched = false;
|
|
198
|
-
for (var i = 0; i < magics.length; i++) {
|
|
199
|
-
if (_matchesAt(buf, entry.offset || 0, magics[i])) { matched = true; break; }
|
|
200
|
-
}
|
|
201
|
-
if (!matched) return false;
|
|
202
|
-
if (typeof entry.extra === "function") {
|
|
203
|
-
try { return !!entry.extra(buf); }
|
|
204
|
-
catch (_e) { return false; }
|
|
205
|
-
}
|
|
206
|
-
return true;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function detect(buf, opts) {
|
|
210
|
-
if (!Buffer.isBuffer(buf)) {
|
|
211
|
-
if (buf instanceof Uint8Array) buf = Buffer.from(buf);
|
|
212
|
-
else return null;
|
|
213
|
-
}
|
|
214
|
-
if (buf.length === 0) return null;
|
|
215
|
-
var registry = SIGNATURES;
|
|
216
|
-
if (opts && Array.isArray(opts.extra) && opts.extra.length > 0) {
|
|
217
|
-
// Operator-extended registry: extras come FIRST so an operator can
|
|
218
|
-
// override a built-in (e.g. tighten OOXML check) without forking.
|
|
219
|
-
registry = opts.extra.concat(SIGNATURES);
|
|
220
|
-
}
|
|
221
|
-
for (var i = 0; i < registry.length; i++) {
|
|
222
|
-
var entry = registry[i];
|
|
223
|
-
if (_entryMatches(entry, buf)) {
|
|
224
|
-
return { mime: entry.mime, extension: entry.extension, category: entry.category, name: entry.name };
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function assertOneOf(buf, allowlist, opts) {
|
|
231
|
-
opts = opts || {};
|
|
232
|
-
if (!Buffer.isBuffer(buf) && !(buf instanceof Uint8Array)) {
|
|
233
|
-
throw _err("BAD_INPUT", "fileType.assertOneOf: input must be a Buffer or Uint8Array, got " + typeof buf);
|
|
234
|
-
}
|
|
235
|
-
if (Buffer.isBuffer(buf) === false) buf = Buffer.from(buf);
|
|
236
|
-
if (buf.length === 0) {
|
|
237
|
-
if (opts.allowEmpty === true) return null;
|
|
238
|
-
throw _err("EMPTY", "fileType.assertOneOf: input is zero bytes");
|
|
239
|
-
}
|
|
240
|
-
if (!Array.isArray(allowlist) || allowlist.length === 0) {
|
|
241
|
-
throw _err("BAD_OPT", "fileType.assertOneOf: allowlist must be a non-empty array");
|
|
242
|
-
}
|
|
243
|
-
var detected = detect(buf, opts);
|
|
244
|
-
if (!detected) {
|
|
245
|
-
throw _err("UNKNOWN_TYPE",
|
|
246
|
-
"fileType.assertOneOf: no signature matched the leading bytes (advertised MIME cannot be trusted alone)");
|
|
247
|
-
}
|
|
248
|
-
// allowlist entries match against `mime` OR `name` OR `category` —
|
|
249
|
-
// operators can pin "image/png" specifically OR "image" for the
|
|
250
|
-
// whole image bucket.
|
|
251
|
-
var allowed = false;
|
|
252
|
-
for (var i = 0; i < allowlist.length; i++) {
|
|
253
|
-
if (allowlist[i] === detected.mime ||
|
|
254
|
-
allowlist[i] === detected.name ||
|
|
255
|
-
allowlist[i] === detected.category) {
|
|
256
|
-
allowed = true; break;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
if (!allowed) {
|
|
260
|
-
throw _err("DISALLOWED_TYPE",
|
|
261
|
-
"fileType.assertOneOf: detected '" + detected.mime + "' (" + detected.name +
|
|
262
|
-
", category=" + detected.category + ") not in allowlist " + JSON.stringify(allowlist));
|
|
263
|
-
}
|
|
264
|
-
return detected;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
module.exports = {
|
|
268
|
-
detect: detect,
|
|
269
|
-
assertOneOf: assertOneOf,
|
|
270
|
-
FileTypeError: FileTypeError,
|
|
271
|
-
// Internal — exposed so tests can introspect the registry shape.
|
|
272
|
-
_SIGNATURES: SIGNATURES,
|
|
273
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* file-type — magic-byte content detection.
|
|
4
|
+
*
|
|
5
|
+
* MIME on a multipart upload comes from the CLIENT — a malicious
|
|
6
|
+
* uploader can label a polyglot HTML payload as "image/png" and the
|
|
7
|
+
* Content-Type header alone won't catch it. This primitive inspects
|
|
8
|
+
* the leading bytes of a buffer against a hardcoded magic-byte
|
|
9
|
+
* registry and returns the actual format independently of the
|
|
10
|
+
* advertised MIME.
|
|
11
|
+
*
|
|
12
|
+
* var detected = b.fileType.detect(buffer);
|
|
13
|
+
* // → { mime: "image/png", extension: "png", category: "image" }
|
|
14
|
+
* // OR null when no signature matches
|
|
15
|
+
*
|
|
16
|
+
* var ok = b.fileType.assertOneOf(buffer, ["image/png", "image/jpeg", "application/pdf"]);
|
|
17
|
+
* // → throws FileTypeError if the actual format isn't in the allowlist
|
|
18
|
+
*
|
|
19
|
+
* Coverage targets the formats most likely to flow through a typical
|
|
20
|
+
* web app's upload boundary: images (PNG/JPEG/GIF/WEBP/AVIF/HEIC),
|
|
21
|
+
* documents (PDF/DOCX/XLSX/PPTX), archives (ZIP/RAR/7Z/TAR/GZ),
|
|
22
|
+
* audio/video (MP3/MP4/WEBM), and a small set of executable formats
|
|
23
|
+
* to deny on upload (PE/ELF/Mach-O). Operators with format coverage
|
|
24
|
+
* outside this list either pass an `extra` registry to extend, or
|
|
25
|
+
* fall back to libmagic via an external sandbox process.
|
|
26
|
+
*
|
|
27
|
+
* Validation policy:
|
|
28
|
+
* - detect(buffer) → returns null on bad input rather than throwing
|
|
29
|
+
* (saved-for-later analysis often runs against partial reads)
|
|
30
|
+
* - assertOneOf(buffer, allowlist[, opts]) throws on mismatch.
|
|
31
|
+
* Operator opt: `allowEmpty: true` to permit zero-length buffers
|
|
32
|
+
* (default false — empty multipart parts are usually a mistake).
|
|
33
|
+
*
|
|
34
|
+
* Out of scope (operator brings their own):
|
|
35
|
+
* - Content disarm (CDR — strip Office macros, PDF JS, etc.).
|
|
36
|
+
* CDR is genuinely hard and format-specific; operators with that
|
|
37
|
+
* requirement reach for a sandbox like dangerzone or vmray.
|
|
38
|
+
* - Polyglot file detection (a single file that is BOTH valid PDF
|
|
39
|
+
* AND valid HTML). detect() returns the first signature match;
|
|
40
|
+
* true polyglot defense needs structural validation per format.
|
|
41
|
+
* - Filename-extension validation. The framework's view is:
|
|
42
|
+
* extensions are operator-controlled metadata, magic bytes are
|
|
43
|
+
* the wire truth.
|
|
44
|
+
*/
|
|
45
|
+
var C = require("./constants");
|
|
46
|
+
var { defineClass } = require("./framework-error");
|
|
47
|
+
|
|
48
|
+
var FileTypeError = defineClass("FileTypeError", { alwaysPermanent: true });
|
|
49
|
+
var _err = FileTypeError.factory;
|
|
50
|
+
|
|
51
|
+
// Cap on bytes inspected for OOXML / WEBP shape-check probes. Set well
|
|
52
|
+
// above any legitimate Office central-directory prefix and any RIFF
|
|
53
|
+
// header — operators don't tune this, the magic-byte registry is fixed.
|
|
54
|
+
var SNIFF_HEAD_BYTES = C.BYTES.kib(4);
|
|
55
|
+
|
|
56
|
+
// Signature registry. Each entry: { name, mime, extension, category,
|
|
57
|
+
// offset, magic: Buffer | [Buffer, ...], extra?: function(buffer)→bool }.
|
|
58
|
+
// Order matters — earlier entries win on ambiguous matches (e.g. ZIP
|
|
59
|
+
// shape catches OOXML, so OOXML-specific entries come FIRST).
|
|
60
|
+
var SIGNATURES = [
|
|
61
|
+
// ---- Office Open XML (DOCX/XLSX/PPTX) ----
|
|
62
|
+
// OOXML files are ZIP archives whose central directory contains
|
|
63
|
+
// [Content_Types].xml. The cheap shape-check is the ZIP local-file
|
|
64
|
+
// header (0x50 0x4B 0x03 0x04) PLUS the filename "[Content_Types].xml"
|
|
65
|
+
// appearing within the first 256 bytes — present for every
|
|
66
|
+
// well-formed OOXML produced by Office / LibreOffice / etc.
|
|
67
|
+
{ name: "docx", mime: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
68
|
+
extension: "docx", category: "document",
|
|
69
|
+
offset: 0, magic: Buffer.from([0x50, 0x4B, 0x03, 0x04]),
|
|
70
|
+
extra: function (buf) {
|
|
71
|
+
var head = buf.subarray(0, Math.min(buf.length, SNIFF_HEAD_BYTES)).toString("binary");
|
|
72
|
+
return head.indexOf("word/") !== -1 || head.indexOf("[Content_Types].xml") !== -1 && head.indexOf("word") !== -1;
|
|
73
|
+
} },
|
|
74
|
+
{ name: "xlsx", mime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
75
|
+
extension: "xlsx", category: "document",
|
|
76
|
+
offset: 0, magic: Buffer.from([0x50, 0x4B, 0x03, 0x04]),
|
|
77
|
+
extra: function (buf) {
|
|
78
|
+
var head = buf.subarray(0, Math.min(buf.length, SNIFF_HEAD_BYTES)).toString("binary");
|
|
79
|
+
return head.indexOf("xl/") !== -1;
|
|
80
|
+
} },
|
|
81
|
+
{ name: "pptx", mime: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
82
|
+
extension: "pptx", category: "document",
|
|
83
|
+
offset: 0, magic: Buffer.from([0x50, 0x4B, 0x03, 0x04]),
|
|
84
|
+
extra: function (buf) {
|
|
85
|
+
var head = buf.subarray(0, Math.min(buf.length, SNIFF_HEAD_BYTES)).toString("binary");
|
|
86
|
+
return head.indexOf("ppt/") !== -1;
|
|
87
|
+
} },
|
|
88
|
+
// ---- Plain ZIP (after OOXML so OOXML wins) ----
|
|
89
|
+
{ name: "zip", mime: "application/zip", extension: "zip", category: "archive",
|
|
90
|
+
offset: 0, magic: [
|
|
91
|
+
Buffer.from([0x50, 0x4B, 0x03, 0x04]), // standard local file header
|
|
92
|
+
Buffer.from([0x50, 0x4B, 0x05, 0x06]), // empty archive
|
|
93
|
+
Buffer.from([0x50, 0x4B, 0x07, 0x08]), // spanned archive
|
|
94
|
+
] },
|
|
95
|
+
|
|
96
|
+
// ---- Images ----
|
|
97
|
+
{ name: "png", mime: "image/png", extension: "png", category: "image",
|
|
98
|
+
offset: 0, magic: Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) },
|
|
99
|
+
{ name: "jpeg", mime: "image/jpeg", extension: "jpg", category: "image",
|
|
100
|
+
offset: 0, magic: Buffer.from([0xFF, 0xD8, 0xFF]) },
|
|
101
|
+
{ name: "gif", mime: "image/gif", extension: "gif", category: "image",
|
|
102
|
+
offset: 0, magic: [Buffer.from("GIF87a", "ascii"), Buffer.from("GIF89a", "ascii")] },
|
|
103
|
+
{ name: "webp", mime: "image/webp", extension: "webp", category: "image",
|
|
104
|
+
offset: 0, magic: Buffer.from("RIFF", "ascii"),
|
|
105
|
+
extra: function (buf) {
|
|
106
|
+
// RIFF header: "RIFF" (0..3), file size (4..7), format tag at offset 8..11.
|
|
107
|
+
return buf.length >= C.BYTES.bytes(12) &&
|
|
108
|
+
buf.subarray(C.BYTES.bytes(0x08), C.BYTES.bytes(12)).toString("ascii") === "WEBP";
|
|
109
|
+
} },
|
|
110
|
+
{ name: "bmp", mime: "image/bmp", extension: "bmp", category: "image",
|
|
111
|
+
offset: 0, magic: Buffer.from([0x42, 0x4D]) },
|
|
112
|
+
{ name: "tiff", mime: "image/tiff", extension: "tiff", category: "image",
|
|
113
|
+
offset: 0, magic: [
|
|
114
|
+
Buffer.from([0x49, 0x49, 0x2A, 0x00]), // little-endian
|
|
115
|
+
Buffer.from([0x4D, 0x4D, 0x00, 0x2A]), // big-endian
|
|
116
|
+
] },
|
|
117
|
+
{ name: "avif", mime: "image/avif", extension: "avif", category: "image",
|
|
118
|
+
offset: 4, magic: Buffer.from("ftypavif", "ascii") },
|
|
119
|
+
{ name: "heic", mime: "image/heic", extension: "heic", category: "image",
|
|
120
|
+
offset: 4, magic: [
|
|
121
|
+
Buffer.from("ftypheic", "ascii"),
|
|
122
|
+
Buffer.from("ftypheix", "ascii"),
|
|
123
|
+
Buffer.from("ftypmif1", "ascii"),
|
|
124
|
+
Buffer.from("ftypmsf1", "ascii"),
|
|
125
|
+
] },
|
|
126
|
+
|
|
127
|
+
// ---- Documents (non-OOXML) ----
|
|
128
|
+
{ name: "pdf", mime: "application/pdf", extension: "pdf", category: "document",
|
|
129
|
+
offset: 0, magic: Buffer.from("%PDF-", "ascii") },
|
|
130
|
+
{ name: "rtf", mime: "application/rtf", extension: "rtf", category: "document",
|
|
131
|
+
offset: 0, magic: Buffer.from("{\\rtf", "ascii") },
|
|
132
|
+
// CFB (Compound File Binary) — old Office (.doc/.xls/.ppt before 2007),
|
|
133
|
+
// also MSI installers.
|
|
134
|
+
{ name: "cfb", mime: "application/x-cfb", extension: "doc", category: "document",
|
|
135
|
+
offset: 0, magic: Buffer.from([0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]) },
|
|
136
|
+
|
|
137
|
+
// ---- Archives ----
|
|
138
|
+
{ name: "rar", mime: "application/vnd.rar", extension: "rar", category: "archive",
|
|
139
|
+
offset: 0, magic: [
|
|
140
|
+
Buffer.from([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00]), // RAR 1.5
|
|
141
|
+
Buffer.from([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00]), // RAR 5
|
|
142
|
+
] },
|
|
143
|
+
{ name: "7z", mime: "application/x-7z-compressed", extension: "7z", category: "archive",
|
|
144
|
+
offset: 0, magic: Buffer.from([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]) },
|
|
145
|
+
{ name: "gz", mime: "application/gzip", extension: "gz", category: "archive",
|
|
146
|
+
offset: 0, magic: Buffer.from([0x1F, 0x8B]) },
|
|
147
|
+
{ name: "bz2", mime: "application/x-bzip2", extension: "bz2", category: "archive",
|
|
148
|
+
offset: 0, magic: Buffer.from("BZh", "ascii") },
|
|
149
|
+
{ name: "xz", mime: "application/x-xz", extension: "xz", category: "archive",
|
|
150
|
+
offset: 0, magic: Buffer.from([0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]) },
|
|
151
|
+
{ name: "tar", mime: "application/x-tar", extension: "tar", category: "archive",
|
|
152
|
+
offset: 257, magic: Buffer.from("ustar", "ascii") },
|
|
153
|
+
|
|
154
|
+
// ---- Audio / video ----
|
|
155
|
+
{ name: "mp3", mime: "audio/mpeg", extension: "mp3", category: "audio",
|
|
156
|
+
offset: 0, magic: [
|
|
157
|
+
Buffer.from([0x49, 0x44, 0x33]), // ID3v2 tag
|
|
158
|
+
Buffer.from([0xFF, 0xFB]), // MPEG audio frame (no ID3)
|
|
159
|
+
Buffer.from([0xFF, 0xF3]),
|
|
160
|
+
Buffer.from([0xFF, 0xF2]),
|
|
161
|
+
] },
|
|
162
|
+
{ name: "mp4", mime: "video/mp4", extension: "mp4", category: "video",
|
|
163
|
+
offset: 4, magic: [
|
|
164
|
+
Buffer.from("ftypisom", "ascii"),
|
|
165
|
+
Buffer.from("ftypiso2", "ascii"),
|
|
166
|
+
Buffer.from("ftypmp42", "ascii"),
|
|
167
|
+
Buffer.from("ftypM4V ", "ascii"),
|
|
168
|
+
] },
|
|
169
|
+
{ name: "webm", mime: "video/webm", extension: "webm", category: "video",
|
|
170
|
+
offset: 0, magic: Buffer.from([0x1A, 0x45, 0xDF, 0xA3]) },
|
|
171
|
+
|
|
172
|
+
// ---- Executables (operators usually DENY these on upload) ----
|
|
173
|
+
{ name: "pe", mime: "application/x-msdownload", extension: "exe", category: "executable",
|
|
174
|
+
offset: 0, magic: Buffer.from([0x4D, 0x5A]) },
|
|
175
|
+
{ name: "elf", mime: "application/x-executable", extension: "elf", category: "executable",
|
|
176
|
+
offset: 0, magic: Buffer.from([0x7F, 0x45, 0x4C, 0x46]) },
|
|
177
|
+
{ name: "macho", mime: "application/x-mach-binary", extension: "macho", category: "executable",
|
|
178
|
+
offset: 0, magic: [
|
|
179
|
+
Buffer.from([0xFE, 0xED, 0xFA, 0xCE]), // big-endian, narrow word
|
|
180
|
+
Buffer.from([0xFE, 0xED, 0xFA, 0xCF]), // big-endian, wide word
|
|
181
|
+
Buffer.from([0xCE, 0xFA, 0xED, 0xFE]), // little-endian, narrow word
|
|
182
|
+
Buffer.from([0xCF, 0xFA, 0xED, 0xFE]), // little-endian, wide word
|
|
183
|
+
Buffer.from([0xCA, 0xFE, 0xBA, 0xBE]), // universal binary (also Java .class)
|
|
184
|
+
] },
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
function _matchesAt(buf, offset, magic) {
|
|
188
|
+
if (buf.length < offset + magic.length) return false;
|
|
189
|
+
for (var i = 0; i < magic.length; i++) {
|
|
190
|
+
if (buf[offset + i] !== magic[i]) return false;
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function _entryMatches(entry, buf) {
|
|
196
|
+
var magics = Array.isArray(entry.magic) ? entry.magic : [entry.magic];
|
|
197
|
+
var matched = false;
|
|
198
|
+
for (var i = 0; i < magics.length; i++) {
|
|
199
|
+
if (_matchesAt(buf, entry.offset || 0, magics[i])) { matched = true; break; }
|
|
200
|
+
}
|
|
201
|
+
if (!matched) return false;
|
|
202
|
+
if (typeof entry.extra === "function") {
|
|
203
|
+
try { return !!entry.extra(buf); }
|
|
204
|
+
catch (_e) { return false; }
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function detect(buf, opts) {
|
|
210
|
+
if (!Buffer.isBuffer(buf)) {
|
|
211
|
+
if (buf instanceof Uint8Array) buf = Buffer.from(buf);
|
|
212
|
+
else return null;
|
|
213
|
+
}
|
|
214
|
+
if (buf.length === 0) return null;
|
|
215
|
+
var registry = SIGNATURES;
|
|
216
|
+
if (opts && Array.isArray(opts.extra) && opts.extra.length > 0) {
|
|
217
|
+
// Operator-extended registry: extras come FIRST so an operator can
|
|
218
|
+
// override a built-in (e.g. tighten OOXML check) without forking.
|
|
219
|
+
registry = opts.extra.concat(SIGNATURES);
|
|
220
|
+
}
|
|
221
|
+
for (var i = 0; i < registry.length; i++) {
|
|
222
|
+
var entry = registry[i];
|
|
223
|
+
if (_entryMatches(entry, buf)) {
|
|
224
|
+
return { mime: entry.mime, extension: entry.extension, category: entry.category, name: entry.name };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function assertOneOf(buf, allowlist, opts) {
|
|
231
|
+
opts = opts || {};
|
|
232
|
+
if (!Buffer.isBuffer(buf) && !(buf instanceof Uint8Array)) {
|
|
233
|
+
throw _err("BAD_INPUT", "fileType.assertOneOf: input must be a Buffer or Uint8Array, got " + typeof buf);
|
|
234
|
+
}
|
|
235
|
+
if (Buffer.isBuffer(buf) === false) buf = Buffer.from(buf);
|
|
236
|
+
if (buf.length === 0) {
|
|
237
|
+
if (opts.allowEmpty === true) return null;
|
|
238
|
+
throw _err("EMPTY", "fileType.assertOneOf: input is zero bytes");
|
|
239
|
+
}
|
|
240
|
+
if (!Array.isArray(allowlist) || allowlist.length === 0) {
|
|
241
|
+
throw _err("BAD_OPT", "fileType.assertOneOf: allowlist must be a non-empty array");
|
|
242
|
+
}
|
|
243
|
+
var detected = detect(buf, opts);
|
|
244
|
+
if (!detected) {
|
|
245
|
+
throw _err("UNKNOWN_TYPE",
|
|
246
|
+
"fileType.assertOneOf: no signature matched the leading bytes (advertised MIME cannot be trusted alone)");
|
|
247
|
+
}
|
|
248
|
+
// allowlist entries match against `mime` OR `name` OR `category` —
|
|
249
|
+
// operators can pin "image/png" specifically OR "image" for the
|
|
250
|
+
// whole image bucket.
|
|
251
|
+
var allowed = false;
|
|
252
|
+
for (var i = 0; i < allowlist.length; i++) {
|
|
253
|
+
if (allowlist[i] === detected.mime ||
|
|
254
|
+
allowlist[i] === detected.name ||
|
|
255
|
+
allowlist[i] === detected.category) {
|
|
256
|
+
allowed = true; break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (!allowed) {
|
|
260
|
+
throw _err("DISALLOWED_TYPE",
|
|
261
|
+
"fileType.assertOneOf: detected '" + detected.mime + "' (" + detected.name +
|
|
262
|
+
", category=" + detected.category + ") not in allowlist " + JSON.stringify(allowlist));
|
|
263
|
+
}
|
|
264
|
+
return detected;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
module.exports = {
|
|
268
|
+
detect: detect,
|
|
269
|
+
assertOneOf: assertOneOf,
|
|
270
|
+
FileTypeError: FileTypeError,
|
|
271
|
+
// Internal — exposed so tests can introspect the registry shape.
|
|
272
|
+
_SIGNATURES: SIGNATURES,
|
|
273
|
+
};
|