@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
|
@@ -1,1091 +1,1170 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* body-parser — request-body buffering + parsing middleware.
|
|
4
|
-
*
|
|
5
|
-
* Populates `req.body` for body-bearing methods (POST, PUT, PATCH,
|
|
6
|
-
* DELETE) based on the request's Content-Type. Five sub-parsers ship,
|
|
7
|
-
* each with its own size-cap, encoding handling, and pollution defense:
|
|
8
|
-
*
|
|
9
|
-
* application/json → req.body = parsed object
|
|
10
|
-
* (via safe-json — POISONED_KEYS
|
|
11
|
-
* stripped, depth + size caps)
|
|
12
|
-
* application/x-www-form-urlencoded → req.body = { field: value }
|
|
13
|
-
* text/plain → req.body = string
|
|
14
|
-
* application/octet-stream → req.body = Buffer (raw)
|
|
15
|
-
* multipart/form-data → req.body = { field: value }
|
|
16
|
-
* req.files = [{ field, filename,
|
|
17
|
-
* mimeType, path, size, hash }]
|
|
18
|
-
* req.filesRejected = [{ field,
|
|
19
|
-
* filename, mimeType, code, message }]
|
|
20
|
-
*
|
|
21
|
-
* Multipart parses incrementally — file parts stream to a tmp dir
|
|
22
|
-
* rather than buffering in memory. Per-file + total-request size caps
|
|
23
|
-
* enforced. Filename sanitization strips path components + traversal.
|
|
24
|
-
* Per-file SHA3-512 hash computed during streaming. Tmp files cleaned
|
|
25
|
-
* on response end (whether the handler returned or threw).
|
|
26
|
-
*
|
|
27
|
-
* var bp = b.middleware.bodyParser({
|
|
28
|
-
* json: {
|
|
29
|
-
* limit: b.constants.BYTES.mib(1),
|
|
30
|
-
* strict: true, // require the body to start with {/[
|
|
31
|
-
* parseHook: function (parsed) { ... return validatedShape; },
|
|
32
|
-
* },
|
|
33
|
-
* urlencoded: {
|
|
34
|
-
* limit: b.constants.BYTES.mib(1),
|
|
35
|
-
* arrayLimit: 100, // for ?tag=a&tag=b → tag: ["a","b"]
|
|
36
|
-
* },
|
|
37
|
-
* text: { limit: b.constants.BYTES.mib(1), charset: "utf-8" },
|
|
38
|
-
* raw: { limit: b.constants.BYTES.mib(10), contentTypes: ["application/octet-stream"] },
|
|
39
|
-
* multipart: {
|
|
40
|
-
* tmpDir: os.tmpdir(),
|
|
41
|
-
* fileSize: b.constants.BYTES.mib(10),
|
|
42
|
-
* totalSize: b.constants.BYTES.mib(50),
|
|
43
|
-
* fileCount: 20,
|
|
44
|
-
* fieldCount: 100,
|
|
45
|
-
* fieldSize: b.constants.BYTES.mib(1),
|
|
46
|
-
* mimeAllowlist: ["image/jpeg", "image/png", "application/pdf"], // null = any
|
|
47
|
-
*
|
|
48
|
-
* // Per-part predicate. Runs after sanitization + MIME checks but
|
|
49
|
-
* // BEFORE the tmp file opens. Rejected parts are SKIPPED — the body
|
|
50
|
-
* // bytes are consumed (we still must scan past them to find the
|
|
51
|
-
* // next boundary) but never written to disk; the part metadata
|
|
52
|
-
* // lands in req.filesRejected. Surviving files appear in req.files
|
|
53
|
-
* // as usual. Sync only — async filtering goes in the route handler.
|
|
54
|
-
* fileFilter: function (part) {
|
|
55
|
-
* // part = { field, filename, mimeType, partHeaders }
|
|
56
|
-
* // return true / undefined → accept
|
|
57
|
-
* // return false → reject silently (entry in req.filesRejected)
|
|
58
|
-
* // return { reject: true, code, message } → reject with custom info
|
|
59
|
-
* return part.field === "avatar" && part.mimeType.startsWith("image/");
|
|
60
|
-
* },
|
|
61
|
-
*
|
|
62
|
-
* // Per-field overrides. maxBytes overrides global fileSize for file
|
|
63
|
-
* // parts and fieldSize for text parts. mimeTypes overrides the
|
|
64
|
-
* // global mimeAllowlist for the named field; other fields still
|
|
65
|
-
* // use the global list.
|
|
66
|
-
* fields: {
|
|
67
|
-
* avatar: { maxBytes: b.constants.BYTES.mib(2), mimeTypes: ["image/jpeg", "image/png"] },
|
|
68
|
-
* document: { maxBytes: b.constants.BYTES.mib(25) },
|
|
69
|
-
* },
|
|
70
|
-
*
|
|
71
|
-
* // When wired, fileFilter rejections emit body-parser.multipart.file_rejected
|
|
72
|
-
* // on the audit chain with the field, filename, mime, and reason.
|
|
73
|
-
* audit: b.audit,
|
|
74
|
-
* },
|
|
75
|
-
* // Stash the raw bytes for webhook-signature paths that need to
|
|
76
|
-
* // verify the wire bytes rather than the parsed shape.
|
|
77
|
-
* keepRawBody: false,
|
|
78
|
-
* });
|
|
79
|
-
* router.use(bp);
|
|
80
|
-
*
|
|
81
|
-
* Set any sub-parser to `false` to disable it entirely (the body is
|
|
82
|
-
* left untouched for those Content-Types — operator handles them).
|
|
83
|
-
*
|
|
84
|
-
* Failure modes — all return responses, do NOT call next(err):
|
|
85
|
-
* 413 Payload Too Large size-cap exceeded (incl. multipart
|
|
86
|
-
* file/total/field caps)
|
|
87
|
-
* 415 Unsupported Media Type Content-Type doesn't match any enabled
|
|
88
|
-
* sub-parser AND the request has a body
|
|
89
|
-
* 400 Bad Request malformed JSON / multipart / urlencoded;
|
|
90
|
-
* filename traversal; MIME not on allowlist
|
|
91
|
-
*
|
|
92
|
-
* Security guarantees:
|
|
93
|
-
* - All parsers enforce size caps BEFORE buffering the full body, so
|
|
94
|
-
* a large body can't OOM the process.
|
|
95
|
-
* - JSON path goes through safe-json — POISONED_KEYS stripped, depth
|
|
96
|
-
* + size caps applied at parse, no prototype pollution downstream.
|
|
97
|
-
* - Urlencoded uses URLSearchParams with a key-count cap, refuses
|
|
98
|
-
* POISONED_KEYS as field names (returns 400).
|
|
99
|
-
* - Multipart filename: path components stripped (basename), traversal
|
|
100
|
-
* dots collapsed, control characters stripped, length capped at 255.
|
|
101
|
-
* Tmp file path is generated by the framework, never derived from
|
|
102
|
-
* the operator-supplied filename — so a malicious filename can't
|
|
103
|
-
* collide with a sensitive path.
|
|
104
|
-
* - Multipart parser refuses fields whose `name` is in POISONED_KEYS
|
|
105
|
-
* (consistent with the JSON path).
|
|
106
|
-
* - Tmp files set with mode 0o600, parent dir created with 0o700.
|
|
107
|
-
* - Cleanup on response end always fires (response close, finish, or
|
|
108
|
-
* error) so a crashing handler doesn't leak files.
|
|
109
|
-
*/
|
|
110
|
-
|
|
111
|
-
var fs = require("fs");
|
|
112
|
-
var os = require("os");
|
|
113
|
-
var path = require("path");
|
|
114
|
-
var nodeCrypto = require("node:crypto");
|
|
115
|
-
var atomicFile = require("../atomic-file");
|
|
116
|
-
var crypto = require("../crypto");
|
|
117
|
-
var requestHelpers = require("../request-helpers");
|
|
118
|
-
var safeBuffer = require("../safe-buffer");
|
|
119
|
-
var safeJson = require("../safe-json");
|
|
120
|
-
var validateOpts = require("../validate-opts");
|
|
121
|
-
var C = require("../constants");
|
|
122
|
-
var { defineClass } = require("../framework-error");
|
|
123
|
-
|
|
124
|
-
var HTTP_STATUS = requestHelpers.HTTP_STATUS;
|
|
125
|
-
var BodyParserError = defineClass("BodyParserError", { withStatusCode: true });
|
|
126
|
-
|
|
127
|
-
// Mirrors safe-json.js + safe-schema.js. Field names that match these
|
|
128
|
-
// are refused at the parse boundary regardless of which sub-parser is
|
|
129
|
-
// in play — consistent prototype-pollution defense across the framework.
|
|
130
|
-
var POISONED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
131
|
-
|
|
132
|
-
// ---- defaults ----
|
|
133
|
-
|
|
134
|
-
var DEFAULTS = Object.freeze({
|
|
135
|
-
json: {
|
|
136
|
-
limit: C.BYTES.mib(1),
|
|
137
|
-
strict: true,
|
|
138
|
-
contentTypes: ["application/json", "application/json; charset=utf-8"],
|
|
139
|
-
charset: "utf-8",
|
|
140
|
-
},
|
|
141
|
-
urlencoded: {
|
|
142
|
-
limit: C.BYTES.mib(1),
|
|
143
|
-
arrayLimit: 100,
|
|
144
|
-
contentTypes: ["application/x-www-form-urlencoded"],
|
|
145
|
-
charset: "utf-8",
|
|
146
|
-
},
|
|
147
|
-
text: {
|
|
148
|
-
limit: C.BYTES.mib(1),
|
|
149
|
-
charset: "utf-8",
|
|
150
|
-
contentTypes: ["text/plain"],
|
|
151
|
-
},
|
|
152
|
-
raw: {
|
|
153
|
-
limit: C.BYTES.mib(10),
|
|
154
|
-
contentTypes: ["application/octet-stream"],
|
|
155
|
-
},
|
|
156
|
-
multipart: {
|
|
157
|
-
tmpDir: null, // resolved per-instance from os.tmpdir()
|
|
158
|
-
fileSize: C.BYTES.mib(10),
|
|
159
|
-
totalSize: C.BYTES.mib(50),
|
|
160
|
-
fileCount: 20,
|
|
161
|
-
fieldCount: 100,
|
|
162
|
-
fieldSize: C.BYTES.mib(1),
|
|
163
|
-
mimeAllowlist: null,
|
|
164
|
-
fileFilter: null, // fn({ field, filename, mimeType, partHeaders }) → bool | { reject, code, message }
|
|
165
|
-
fields: null, // per-field overrides: { name: { maxBytes?, mimeTypes? } }
|
|
166
|
-
audit: null, // when wired, file-rejection emits an audit event
|
|
167
|
-
contentTypes: ["multipart/form-data"],
|
|
168
|
-
},
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
var BODY_BEARING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
172
|
-
|
|
173
|
-
// Headroom on top of arrayLimit before declaring a key-bomb. Sized so a
|
|
174
|
-
// reasonable form (a few hundred fields with repeated names) fits without
|
|
175
|
-
// tripping the cap.
|
|
176
|
-
var URLENCODED_KEY_HEADROOM = C.BYTES.bytes(1000);
|
|
177
|
-
|
|
178
|
-
// ---- helpers ----
|
|
179
|
-
|
|
180
|
-
function _contentType(req) {
|
|
181
|
-
var ct = req.headers && req.headers["content-type"];
|
|
182
|
-
if (typeof ct !== "string") return { type: "", params: {} };
|
|
183
|
-
var idx = ct.indexOf(";");
|
|
184
|
-
var type = (idx === -1 ? ct : ct.slice(0, idx)).trim().toLowerCase();
|
|
185
|
-
var params = {};
|
|
186
|
-
if (idx !== -1) {
|
|
187
|
-
var rest = ct.slice(idx + 1);
|
|
188
|
-
var parts = rest.split(";");
|
|
189
|
-
for (var i = 0; i < parts.length; i++) {
|
|
190
|
-
var p = parts[i].trim();
|
|
191
|
-
var eq = p.indexOf("=");
|
|
192
|
-
if (eq === -1) continue;
|
|
193
|
-
var k = p.slice(0, eq).trim().toLowerCase();
|
|
194
|
-
var v = p.slice(eq + 1).trim();
|
|
195
|
-
if (v.length >= 2 && v[0] === '"' && v[v.length - 1] === '"') v = v.slice(1, -1);
|
|
196
|
-
params[k] = v;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return { type: type, params: params };
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function _typeMatches(actual, allowed) {
|
|
203
|
-
for (var i = 0; i < allowed.length; i++) {
|
|
204
|
-
var a = allowed[i].toLowerCase();
|
|
205
|
-
// Match either exact type or "type/*" prefix
|
|
206
|
-
if (a === actual) return true;
|
|
207
|
-
var slash = a.indexOf("/");
|
|
208
|
-
if (slash !== -1 && a.slice(slash + 1) === "*" &&
|
|
209
|
-
actual.indexOf(a.slice(0, slash + 1)) === 0) return true;
|
|
210
|
-
}
|
|
211
|
-
return false;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// RFC 9112 §6.1: Content-Length MUST be a sequence of decimal digits with
|
|
215
|
-
// no whitespace, sign, or trailing garbage. parseInt("123abc") returning
|
|
216
|
-
// 123 is the lenient parse that lets malformed headers slip past the
|
|
217
|
-
// preflight cap; the strict regex catches them at the boundary.
|
|
218
|
-
var STRICT_CONTENT_LENGTH = /^\d+$/;
|
|
219
|
-
|
|
220
|
-
function _parseContentLength(cl) {
|
|
221
|
-
if (typeof cl !== "string" || !STRICT_CONTENT_LENGTH.test(cl)) return null;
|
|
222
|
-
var n = Number(cl);
|
|
223
|
-
return isFinite(n) ? n : null;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function _hasBody(req) {
|
|
227
|
-
if (!BODY_BEARING_METHODS.has(req.method)) return false;
|
|
228
|
-
var cl = req.headers && req.headers["content-length"];
|
|
229
|
-
if (typeof cl === "string") {
|
|
230
|
-
var clNum = _parseContentLength(cl);
|
|
231
|
-
// Spec-shaped zero (the only RFC 9112 §6.1 zero) → no body. Malformed
|
|
232
|
-
// values (non-decimal-digits) flow through as "yes, has body" so the
|
|
233
|
-
// downstream _bufferBody call rejects with 400 — silently treating
|
|
234
|
-
// a malformed header as "no body" would let the request slip past
|
|
235
|
-
// the parser entirely.
|
|
236
|
-
if (clNum === 0) return false;
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
239
|
-
var te = req.headers && req.headers["transfer-encoding"];
|
|
240
|
-
if (typeof te === "string" && te.length > 0) return true;
|
|
241
|
-
return false;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
var
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
return
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
var
|
|
520
|
-
var
|
|
521
|
-
var
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
var
|
|
537
|
-
|
|
538
|
-
var
|
|
539
|
-
|
|
540
|
-
var
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
if (
|
|
673
|
-
|
|
674
|
-
if (
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
//
|
|
745
|
-
|
|
746
|
-
var
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
try {
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
//
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* body-parser — request-body buffering + parsing middleware.
|
|
4
|
+
*
|
|
5
|
+
* Populates `req.body` for body-bearing methods (POST, PUT, PATCH,
|
|
6
|
+
* DELETE) based on the request's Content-Type. Five sub-parsers ship,
|
|
7
|
+
* each with its own size-cap, encoding handling, and pollution defense:
|
|
8
|
+
*
|
|
9
|
+
* application/json → req.body = parsed object
|
|
10
|
+
* (via safe-json — POISONED_KEYS
|
|
11
|
+
* stripped, depth + size caps)
|
|
12
|
+
* application/x-www-form-urlencoded → req.body = { field: value }
|
|
13
|
+
* text/plain → req.body = string
|
|
14
|
+
* application/octet-stream → req.body = Buffer (raw)
|
|
15
|
+
* multipart/form-data → req.body = { field: value }
|
|
16
|
+
* req.files = [{ field, filename,
|
|
17
|
+
* mimeType, path, size, hash }]
|
|
18
|
+
* req.filesRejected = [{ field,
|
|
19
|
+
* filename, mimeType, code, message }]
|
|
20
|
+
*
|
|
21
|
+
* Multipart parses incrementally — file parts stream to a tmp dir
|
|
22
|
+
* rather than buffering in memory. Per-file + total-request size caps
|
|
23
|
+
* enforced. Filename sanitization strips path components + traversal.
|
|
24
|
+
* Per-file SHA3-512 hash computed during streaming. Tmp files cleaned
|
|
25
|
+
* on response end (whether the handler returned or threw).
|
|
26
|
+
*
|
|
27
|
+
* var bp = b.middleware.bodyParser({
|
|
28
|
+
* json: {
|
|
29
|
+
* limit: b.constants.BYTES.mib(1),
|
|
30
|
+
* strict: true, // require the body to start with {/[
|
|
31
|
+
* parseHook: function (parsed) { ... return validatedShape; },
|
|
32
|
+
* },
|
|
33
|
+
* urlencoded: {
|
|
34
|
+
* limit: b.constants.BYTES.mib(1),
|
|
35
|
+
* arrayLimit: 100, // for ?tag=a&tag=b → tag: ["a","b"]
|
|
36
|
+
* },
|
|
37
|
+
* text: { limit: b.constants.BYTES.mib(1), charset: "utf-8" },
|
|
38
|
+
* raw: { limit: b.constants.BYTES.mib(10), contentTypes: ["application/octet-stream"] },
|
|
39
|
+
* multipart: {
|
|
40
|
+
* tmpDir: os.tmpdir(),
|
|
41
|
+
* fileSize: b.constants.BYTES.mib(10),
|
|
42
|
+
* totalSize: b.constants.BYTES.mib(50),
|
|
43
|
+
* fileCount: 20,
|
|
44
|
+
* fieldCount: 100,
|
|
45
|
+
* fieldSize: b.constants.BYTES.mib(1),
|
|
46
|
+
* mimeAllowlist: ["image/jpeg", "image/png", "application/pdf"], // null = any
|
|
47
|
+
*
|
|
48
|
+
* // Per-part predicate. Runs after sanitization + MIME checks but
|
|
49
|
+
* // BEFORE the tmp file opens. Rejected parts are SKIPPED — the body
|
|
50
|
+
* // bytes are consumed (we still must scan past them to find the
|
|
51
|
+
* // next boundary) but never written to disk; the part metadata
|
|
52
|
+
* // lands in req.filesRejected. Surviving files appear in req.files
|
|
53
|
+
* // as usual. Sync only — async filtering goes in the route handler.
|
|
54
|
+
* fileFilter: function (part) {
|
|
55
|
+
* // part = { field, filename, mimeType, partHeaders }
|
|
56
|
+
* // return true / undefined → accept
|
|
57
|
+
* // return false → reject silently (entry in req.filesRejected)
|
|
58
|
+
* // return { reject: true, code, message } → reject with custom info
|
|
59
|
+
* return part.field === "avatar" && part.mimeType.startsWith("image/");
|
|
60
|
+
* },
|
|
61
|
+
*
|
|
62
|
+
* // Per-field overrides. maxBytes overrides global fileSize for file
|
|
63
|
+
* // parts and fieldSize for text parts. mimeTypes overrides the
|
|
64
|
+
* // global mimeAllowlist for the named field; other fields still
|
|
65
|
+
* // use the global list.
|
|
66
|
+
* fields: {
|
|
67
|
+
* avatar: { maxBytes: b.constants.BYTES.mib(2), mimeTypes: ["image/jpeg", "image/png"] },
|
|
68
|
+
* document: { maxBytes: b.constants.BYTES.mib(25) },
|
|
69
|
+
* },
|
|
70
|
+
*
|
|
71
|
+
* // When wired, fileFilter rejections emit body-parser.multipart.file_rejected
|
|
72
|
+
* // on the audit chain with the field, filename, mime, and reason.
|
|
73
|
+
* audit: b.audit,
|
|
74
|
+
* },
|
|
75
|
+
* // Stash the raw bytes for webhook-signature paths that need to
|
|
76
|
+
* // verify the wire bytes rather than the parsed shape.
|
|
77
|
+
* keepRawBody: false,
|
|
78
|
+
* });
|
|
79
|
+
* router.use(bp);
|
|
80
|
+
*
|
|
81
|
+
* Set any sub-parser to `false` to disable it entirely (the body is
|
|
82
|
+
* left untouched for those Content-Types — operator handles them).
|
|
83
|
+
*
|
|
84
|
+
* Failure modes — all return responses, do NOT call next(err):
|
|
85
|
+
* 413 Payload Too Large size-cap exceeded (incl. multipart
|
|
86
|
+
* file/total/field caps)
|
|
87
|
+
* 415 Unsupported Media Type Content-Type doesn't match any enabled
|
|
88
|
+
* sub-parser AND the request has a body
|
|
89
|
+
* 400 Bad Request malformed JSON / multipart / urlencoded;
|
|
90
|
+
* filename traversal; MIME not on allowlist
|
|
91
|
+
*
|
|
92
|
+
* Security guarantees:
|
|
93
|
+
* - All parsers enforce size caps BEFORE buffering the full body, so
|
|
94
|
+
* a large body can't OOM the process.
|
|
95
|
+
* - JSON path goes through safe-json — POISONED_KEYS stripped, depth
|
|
96
|
+
* + size caps applied at parse, no prototype pollution downstream.
|
|
97
|
+
* - Urlencoded uses URLSearchParams with a key-count cap, refuses
|
|
98
|
+
* POISONED_KEYS as field names (returns 400).
|
|
99
|
+
* - Multipart filename: path components stripped (basename), traversal
|
|
100
|
+
* dots collapsed, control characters stripped, length capped at 255.
|
|
101
|
+
* Tmp file path is generated by the framework, never derived from
|
|
102
|
+
* the operator-supplied filename — so a malicious filename can't
|
|
103
|
+
* collide with a sensitive path.
|
|
104
|
+
* - Multipart parser refuses fields whose `name` is in POISONED_KEYS
|
|
105
|
+
* (consistent with the JSON path).
|
|
106
|
+
* - Tmp files set with mode 0o600, parent dir created with 0o700.
|
|
107
|
+
* - Cleanup on response end always fires (response close, finish, or
|
|
108
|
+
* error) so a crashing handler doesn't leak files.
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
var fs = require("fs");
|
|
112
|
+
var os = require("os");
|
|
113
|
+
var path = require("path");
|
|
114
|
+
var nodeCrypto = require("node:crypto");
|
|
115
|
+
var atomicFile = require("../atomic-file");
|
|
116
|
+
var crypto = require("../crypto");
|
|
117
|
+
var requestHelpers = require("../request-helpers");
|
|
118
|
+
var safeBuffer = require("../safe-buffer");
|
|
119
|
+
var safeJson = require("../safe-json");
|
|
120
|
+
var validateOpts = require("../validate-opts");
|
|
121
|
+
var C = require("../constants");
|
|
122
|
+
var { defineClass } = require("../framework-error");
|
|
123
|
+
|
|
124
|
+
var HTTP_STATUS = requestHelpers.HTTP_STATUS;
|
|
125
|
+
var BodyParserError = defineClass("BodyParserError", { withStatusCode: true });
|
|
126
|
+
|
|
127
|
+
// Mirrors safe-json.js + safe-schema.js. Field names that match these
|
|
128
|
+
// are refused at the parse boundary regardless of which sub-parser is
|
|
129
|
+
// in play — consistent prototype-pollution defense across the framework.
|
|
130
|
+
var POISONED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
131
|
+
|
|
132
|
+
// ---- defaults ----
|
|
133
|
+
|
|
134
|
+
var DEFAULTS = Object.freeze({
|
|
135
|
+
json: {
|
|
136
|
+
limit: C.BYTES.mib(1),
|
|
137
|
+
strict: true,
|
|
138
|
+
contentTypes: ["application/json", "application/json; charset=utf-8"],
|
|
139
|
+
charset: "utf-8",
|
|
140
|
+
},
|
|
141
|
+
urlencoded: {
|
|
142
|
+
limit: C.BYTES.mib(1),
|
|
143
|
+
arrayLimit: 100,
|
|
144
|
+
contentTypes: ["application/x-www-form-urlencoded"],
|
|
145
|
+
charset: "utf-8",
|
|
146
|
+
},
|
|
147
|
+
text: {
|
|
148
|
+
limit: C.BYTES.mib(1),
|
|
149
|
+
charset: "utf-8",
|
|
150
|
+
contentTypes: ["text/plain"],
|
|
151
|
+
},
|
|
152
|
+
raw: {
|
|
153
|
+
limit: C.BYTES.mib(10),
|
|
154
|
+
contentTypes: ["application/octet-stream"],
|
|
155
|
+
},
|
|
156
|
+
multipart: {
|
|
157
|
+
tmpDir: null, // resolved per-instance from os.tmpdir()
|
|
158
|
+
fileSize: C.BYTES.mib(10),
|
|
159
|
+
totalSize: C.BYTES.mib(50),
|
|
160
|
+
fileCount: 20,
|
|
161
|
+
fieldCount: 100,
|
|
162
|
+
fieldSize: C.BYTES.mib(1),
|
|
163
|
+
mimeAllowlist: null,
|
|
164
|
+
fileFilter: null, // fn({ field, filename, mimeType, partHeaders }) → bool | { reject, code, message }
|
|
165
|
+
fields: null, // per-field overrides: { name: { maxBytes?, mimeTypes? } }
|
|
166
|
+
audit: null, // when wired, file-rejection emits an audit event
|
|
167
|
+
contentTypes: ["multipart/form-data"],
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
var BODY_BEARING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
172
|
+
|
|
173
|
+
// Headroom on top of arrayLimit before declaring a key-bomb. Sized so a
|
|
174
|
+
// reasonable form (a few hundred fields with repeated names) fits without
|
|
175
|
+
// tripping the cap.
|
|
176
|
+
var URLENCODED_KEY_HEADROOM = C.BYTES.bytes(1000);
|
|
177
|
+
|
|
178
|
+
// ---- helpers ----
|
|
179
|
+
|
|
180
|
+
function _contentType(req) {
|
|
181
|
+
var ct = req.headers && req.headers["content-type"];
|
|
182
|
+
if (typeof ct !== "string") return { type: "", params: {} };
|
|
183
|
+
var idx = ct.indexOf(";");
|
|
184
|
+
var type = (idx === -1 ? ct : ct.slice(0, idx)).trim().toLowerCase();
|
|
185
|
+
var params = {};
|
|
186
|
+
if (idx !== -1) {
|
|
187
|
+
var rest = ct.slice(idx + 1);
|
|
188
|
+
var parts = rest.split(";");
|
|
189
|
+
for (var i = 0; i < parts.length; i++) {
|
|
190
|
+
var p = parts[i].trim();
|
|
191
|
+
var eq = p.indexOf("=");
|
|
192
|
+
if (eq === -1) continue;
|
|
193
|
+
var k = p.slice(0, eq).trim().toLowerCase();
|
|
194
|
+
var v = p.slice(eq + 1).trim();
|
|
195
|
+
if (v.length >= 2 && v[0] === '"' && v[v.length - 1] === '"') v = v.slice(1, -1);
|
|
196
|
+
params[k] = v;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { type: type, params: params };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function _typeMatches(actual, allowed) {
|
|
203
|
+
for (var i = 0; i < allowed.length; i++) {
|
|
204
|
+
var a = allowed[i].toLowerCase();
|
|
205
|
+
// Match either exact type or "type/*" prefix
|
|
206
|
+
if (a === actual) return true;
|
|
207
|
+
var slash = a.indexOf("/");
|
|
208
|
+
if (slash !== -1 && a.slice(slash + 1) === "*" &&
|
|
209
|
+
actual.indexOf(a.slice(0, slash + 1)) === 0) return true;
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// RFC 9112 §6.1: Content-Length MUST be a sequence of decimal digits with
|
|
215
|
+
// no whitespace, sign, or trailing garbage. parseInt("123abc") returning
|
|
216
|
+
// 123 is the lenient parse that lets malformed headers slip past the
|
|
217
|
+
// preflight cap; the strict regex catches them at the boundary.
|
|
218
|
+
var STRICT_CONTENT_LENGTH = /^\d+$/;
|
|
219
|
+
|
|
220
|
+
function _parseContentLength(cl) {
|
|
221
|
+
if (typeof cl !== "string" || !STRICT_CONTENT_LENGTH.test(cl)) return null;
|
|
222
|
+
var n = Number(cl);
|
|
223
|
+
return isFinite(n) ? n : null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function _hasBody(req) {
|
|
227
|
+
if (!BODY_BEARING_METHODS.has(req.method)) return false;
|
|
228
|
+
var cl = req.headers && req.headers["content-length"];
|
|
229
|
+
if (typeof cl === "string") {
|
|
230
|
+
var clNum = _parseContentLength(cl);
|
|
231
|
+
// Spec-shaped zero (the only RFC 9112 §6.1 zero) → no body. Malformed
|
|
232
|
+
// values (non-decimal-digits) flow through as "yes, has body" so the
|
|
233
|
+
// downstream _bufferBody call rejects with 400 — silently treating
|
|
234
|
+
// a malformed header as "no body" would let the request slip past
|
|
235
|
+
// the parser entirely.
|
|
236
|
+
if (clNum === 0) return false;
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
var te = req.headers && req.headers["transfer-encoding"];
|
|
240
|
+
if (typeof te === "string" && te.length > 0) return true;
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// HTTP request-smuggling defense per RFC 9112 §6.1 — covers the
|
|
245
|
+
// CVE-2022-31394 / CVE-2024-27316 / CL.TE / TE.CL / TE.TE class.
|
|
246
|
+
// Returns null on clean; { status, code, message } on smuggling-shaped
|
|
247
|
+
// request that the caller MUST reject with 400 + Connection: close.
|
|
248
|
+
function _detectSmuggling(req) {
|
|
249
|
+
var headers = req.headers || {};
|
|
250
|
+
var cl = headers["content-length"];
|
|
251
|
+
var te = headers["transfer-encoding"];
|
|
252
|
+
|
|
253
|
+
// 1. Both Content-Length AND Transfer-Encoding present — RFC 9112
|
|
254
|
+
// §6.1 says receiver MUST reject; the dual presence is the canonical
|
|
255
|
+
// CL.TE / TE.CL smuggling shape.
|
|
256
|
+
if (typeof cl === "string" && cl.length > 0 &&
|
|
257
|
+
typeof te === "string" && te.length > 0) {
|
|
258
|
+
return {
|
|
259
|
+
status: HTTP_STATUS.BAD_REQUEST, code: "smuggling/te-cl-conflict",
|
|
260
|
+
message: "request has both Content-Length and Transfer-Encoding " +
|
|
261
|
+
"headers (RFC 9112 §6.1 — request-smuggling vector)",
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 2. Multiple Content-Length values. Node's http parser collapses
|
|
266
|
+
// duplicate headers into a comma-separated string — `cl.indexOf(",")`
|
|
267
|
+
// catches it.
|
|
268
|
+
if (typeof cl === "string" && cl.indexOf(",") !== -1) {
|
|
269
|
+
return {
|
|
270
|
+
status: HTTP_STATUS.BAD_REQUEST, code: "smuggling/multiple-content-length",
|
|
271
|
+
message: "request has multiple Content-Length values (RFC 9112 §6.1)",
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 3. Transfer-Encoding present — final coding MUST be `chunked`
|
|
276
|
+
// (RFC 9112 §6.1). Anything else is a smuggling vector or
|
|
277
|
+
// server-side decode error.
|
|
278
|
+
if (typeof te === "string" && te.length > 0) {
|
|
279
|
+
var tokens = te.toLowerCase().split(",").map(function (t) { return t.trim(); });
|
|
280
|
+
var last = tokens[tokens.length - 1];
|
|
281
|
+
if (last !== "chunked") {
|
|
282
|
+
return {
|
|
283
|
+
status: HTTP_STATUS.BAD_REQUEST, code: "smuggling/te-not-chunked",
|
|
284
|
+
message: "request has Transfer-Encoding but final coding is not " +
|
|
285
|
+
"`chunked` (RFC 9112 §6.1 requires chunked be last)",
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
// 4. Duplicate `chunked` token (TE: chunked, chunked) — explicitly
|
|
289
|
+
// forbidden by RFC 9112 §6.1.
|
|
290
|
+
var chunkedCount = 0;
|
|
291
|
+
for (var i = 0; i < tokens.length; i += 1) {
|
|
292
|
+
if (tokens[i] === "chunked") chunkedCount += 1;
|
|
293
|
+
}
|
|
294
|
+
if (chunkedCount > 1) {
|
|
295
|
+
return {
|
|
296
|
+
status: HTTP_STATUS.BAD_REQUEST, code: "smuggling/duplicate-chunked",
|
|
297
|
+
message: "Transfer-Encoding lists `chunked` more than once " +
|
|
298
|
+
"(RFC 9112 §6.1 — TE.TE smuggling vector)",
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function _writeError(res, status, message, code) {
|
|
307
|
+
if (res.headersSent) return;
|
|
308
|
+
var body = JSON.stringify({ error: message, code: code });
|
|
309
|
+
res.writeHead(status, {
|
|
310
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
311
|
+
"Content-Length": Buffer.byteLength(body),
|
|
312
|
+
});
|
|
313
|
+
res.end(body);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Buffer the raw body up to `limit`. If Content-Length is known and
|
|
317
|
+
// exceeds limit, reject IMMEDIATELY without reading any bytes.
|
|
318
|
+
function _bufferBody(req, limit) {
|
|
319
|
+
return new Promise(function (resolve, reject) {
|
|
320
|
+
var cl = req.headers && req.headers["content-length"];
|
|
321
|
+
if (typeof cl === "string") {
|
|
322
|
+
var clNum = _parseContentLength(cl);
|
|
323
|
+
if (clNum === null) {
|
|
324
|
+
// RFC 9112 §6.1 — malformed Content-Length is a 400.
|
|
325
|
+
reject(new BodyParserError(
|
|
326
|
+
"body-parser/bad-content-length",
|
|
327
|
+
"Content-Length is not a sequence of decimal digits: " + JSON.stringify(cl),
|
|
328
|
+
true, HTTP_STATUS.BAD_REQUEST
|
|
329
|
+
));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (clNum > limit) {
|
|
333
|
+
reject(new BodyParserError(
|
|
334
|
+
"body-parser/too-large",
|
|
335
|
+
"request body exceeds limit (" + clNum + " > " + limit + ")",
|
|
336
|
+
true, HTTP_STATUS.PAYLOAD_TOO_LARGE
|
|
337
|
+
));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
var collector = safeBuffer.boundedChunkCollector({
|
|
342
|
+
maxBytes: limit,
|
|
343
|
+
errorClass: BodyParserError,
|
|
344
|
+
sizeCode: "body-parser/too-large",
|
|
345
|
+
sizeMessage: "request body exceeds limit",
|
|
346
|
+
});
|
|
347
|
+
var done = false;
|
|
348
|
+
req.on("data", function (chunk) {
|
|
349
|
+
if (done) return;
|
|
350
|
+
try { collector.push(chunk); }
|
|
351
|
+
catch (e) {
|
|
352
|
+
done = true;
|
|
353
|
+
try { req.destroy(); } catch (_e) { /* socket already closed */ }
|
|
354
|
+
if (e && e.isBodyParserError) e.statusCode = HTTP_STATUS.PAYLOAD_TOO_LARGE;
|
|
355
|
+
reject(e);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
req.on("end", function () { if (!done) { done = true; resolve(collector.result()); } });
|
|
360
|
+
req.on("error", function (e) { if (!done) { done = true; reject(e); } });
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ---- JSON parser ----
|
|
365
|
+
|
|
366
|
+
async function _parseJson(req, opts) {
|
|
367
|
+
var buf = await _bufferBody(req, opts.limit);
|
|
368
|
+
if (buf.length === 0) return undefined;
|
|
369
|
+
var text = buf.toString(opts.charset);
|
|
370
|
+
if (opts.strict) {
|
|
371
|
+
var head = text.replace(/^[\s\u00A0\uFEFF]+/, "")[0];
|
|
372
|
+
if (head !== "{" && head !== "[") {
|
|
373
|
+
throw new BodyParserError(
|
|
374
|
+
"body-parser/json-strict",
|
|
375
|
+
"JSON body must start with '{' or '[' (strict mode)",
|
|
376
|
+
true, HTTP_STATUS.BAD_REQUEST
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
var parsed;
|
|
381
|
+
try {
|
|
382
|
+
// safe-json strips POISONED_KEYS and enforces depth + byte caps.
|
|
383
|
+
parsed = safeJson.parse(text, { maxBytes: opts.limit });
|
|
384
|
+
} catch (e) {
|
|
385
|
+
throw new BodyParserError(
|
|
386
|
+
"body-parser/json-malformed",
|
|
387
|
+
"JSON parse failed: " + ((e && e.message) || String(e)),
|
|
388
|
+
true, HTTP_STATUS.BAD_REQUEST
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
if (typeof opts.parseHook === "function") {
|
|
392
|
+
try { parsed = opts.parseHook(parsed); }
|
|
393
|
+
catch (e) {
|
|
394
|
+
throw new BodyParserError(
|
|
395
|
+
"body-parser/json-hook",
|
|
396
|
+
"JSON parseHook failed: " + ((e && e.message) || String(e)),
|
|
397
|
+
true, HTTP_STATUS.BAD_REQUEST
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return parsed;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ---- urlencoded parser ----
|
|
405
|
+
|
|
406
|
+
async function _parseUrlencoded(req, opts) {
|
|
407
|
+
var buf = await _bufferBody(req, opts.limit);
|
|
408
|
+
if (buf.length === 0) return {};
|
|
409
|
+
var text = buf.toString(opts.charset);
|
|
410
|
+
var sp;
|
|
411
|
+
try { sp = new URLSearchParams(text); }
|
|
412
|
+
catch (e) {
|
|
413
|
+
throw new BodyParserError(
|
|
414
|
+
"body-parser/urlencoded-malformed",
|
|
415
|
+
"urlencoded parse failed: " + ((e && e.message) || String(e)),
|
|
416
|
+
true, HTTP_STATUS.BAD_REQUEST
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
var out = {};
|
|
420
|
+
var keyCount = 0;
|
|
421
|
+
// Track repeated keys so [a=1, a=2] becomes a: ["1","2"] rather than overwriting.
|
|
422
|
+
var seen = Object.create(null);
|
|
423
|
+
var keys = [];
|
|
424
|
+
sp.forEach(function (value, key) { keys.push([key, value]); });
|
|
425
|
+
for (var i = 0; i < keys.length; i++) {
|
|
426
|
+
var k = keys[i][0];
|
|
427
|
+
var v = keys[i][1];
|
|
428
|
+
if (POISONED_KEYS.has(k)) {
|
|
429
|
+
throw new BodyParserError(
|
|
430
|
+
"body-parser/urlencoded-poisoned-key",
|
|
431
|
+
"urlencoded body contains forbidden key '" + k + "' (prototype-pollution defense)",
|
|
432
|
+
true, HTTP_STATUS.BAD_REQUEST
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
keyCount++;
|
|
436
|
+
if (keyCount > opts.arrayLimit + URLENCODED_KEY_HEADROOM) { // soft cap on total keys
|
|
437
|
+
throw new BodyParserError(
|
|
438
|
+
"body-parser/urlencoded-too-many-fields",
|
|
439
|
+
"urlencoded body has too many fields",
|
|
440
|
+
true, 413
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
if (Object.prototype.hasOwnProperty.call(seen, k)) {
|
|
444
|
+
if (Array.isArray(out[k])) {
|
|
445
|
+
if (out[k].length >= opts.arrayLimit) {
|
|
446
|
+
throw new BodyParserError(
|
|
447
|
+
"body-parser/urlencoded-array-too-large",
|
|
448
|
+
"urlencoded array '" + k + "' exceeds arrayLimit (" + opts.arrayLimit + ")",
|
|
449
|
+
true, 413
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
out[k].push(v);
|
|
453
|
+
} else {
|
|
454
|
+
out[k] = [out[k], v];
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
out[k] = v;
|
|
458
|
+
seen[k] = true;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return out;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ---- text parser ----
|
|
465
|
+
|
|
466
|
+
async function _parseText(req, opts) {
|
|
467
|
+
var buf = await _bufferBody(req, opts.limit);
|
|
468
|
+
return buf.toString(opts.charset);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ---- raw parser ----
|
|
472
|
+
|
|
473
|
+
async function _parseRaw(req, opts) {
|
|
474
|
+
return await _bufferBody(req, opts.limit);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ---- multipart parser ----
|
|
478
|
+
//
|
|
479
|
+
// Streaming RFC 7578 multipart/form-data parser. Walks an incoming
|
|
480
|
+
// request stream byte-by-byte (well, chunk-by-chunk with a sliding
|
|
481
|
+
// look-ahead window) and emits parts as they're encountered. File
|
|
482
|
+
// parts stream straight to disk; field parts buffer in memory up to
|
|
483
|
+
// fieldSize.
|
|
484
|
+
//
|
|
485
|
+
// State machine:
|
|
486
|
+
// INITIAL skip preamble until first boundary
|
|
487
|
+
// AFTER_BD just consumed a boundary; check next two bytes for
|
|
488
|
+
// "--" (end-of-multipart) or "\r\n" (next part)
|
|
489
|
+
// HEADERS reading per-part headers until \r\n\r\n
|
|
490
|
+
// BODY streaming part body; watch for \r\n--<boundary>
|
|
491
|
+
// DONE all parts read
|
|
492
|
+
|
|
493
|
+
var MP_INITIAL = 0;
|
|
494
|
+
var MP_AFTER_BD = 1;
|
|
495
|
+
var MP_HEADERS = 2;
|
|
496
|
+
var MP_BODY = 3;
|
|
497
|
+
var MP_DONE = 4;
|
|
498
|
+
|
|
499
|
+
function _sanitizeFilename(name) {
|
|
500
|
+
if (typeof name !== "string") return null;
|
|
501
|
+
// Strip every path component — keep only basename (last segment).
|
|
502
|
+
// Both POSIX and Windows separators handled, plus URL-encoded.
|
|
503
|
+
var s = name.replace(/\\/g, "/");
|
|
504
|
+
var idx = s.lastIndexOf("/");
|
|
505
|
+
if (idx !== -1) s = s.slice(idx + 1);
|
|
506
|
+
// Drop control characters, NUL, leading/trailing dots.
|
|
507
|
+
s = s.replace(/\p{Cc}/gu, "");
|
|
508
|
+
s = s.replace(/^\.+/, "").replace(/\.+$/, "");
|
|
509
|
+
if (s.length === 0) return null;
|
|
510
|
+
if (s.length > 255) s = s.slice(0, 255);
|
|
511
|
+
// Refuse any remaining traversal artifact.
|
|
512
|
+
if (s === "." || s === "..") return null;
|
|
513
|
+
return s;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function _parseMultipartHeaders(rawHeaders) {
|
|
517
|
+
// Each line is `Header-Name: value`. Common headers: Content-Disposition,
|
|
518
|
+
// Content-Type, Content-Transfer-Encoding. Unknown headers are ignored.
|
|
519
|
+
var lines = rawHeaders.split("\r\n");
|
|
520
|
+
var out = {};
|
|
521
|
+
for (var i = 0; i < lines.length; i++) {
|
|
522
|
+
var line = lines[i];
|
|
523
|
+
if (!line) continue;
|
|
524
|
+
var idx = line.indexOf(":");
|
|
525
|
+
if (idx === -1) continue;
|
|
526
|
+
var k = line.slice(0, idx).trim().toLowerCase();
|
|
527
|
+
var v = line.slice(idx + 1).trim();
|
|
528
|
+
out[k] = v;
|
|
529
|
+
}
|
|
530
|
+
return out;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function _parseHeaderParams(headerValue) {
|
|
534
|
+
// Content-Disposition: form-data; name="field"; filename="x.txt"
|
|
535
|
+
// Returns { _value: "form-data", name: "field", filename: "x.txt" }
|
|
536
|
+
var out = { _value: "" };
|
|
537
|
+
if (!headerValue) return out;
|
|
538
|
+
var parts = headerValue.split(";");
|
|
539
|
+
out._value = parts[0].trim().toLowerCase();
|
|
540
|
+
for (var i = 1; i < parts.length; i++) {
|
|
541
|
+
var p = parts[i].trim();
|
|
542
|
+
var eq = p.indexOf("=");
|
|
543
|
+
if (eq === -1) continue;
|
|
544
|
+
var k = p.slice(0, eq).trim().toLowerCase();
|
|
545
|
+
var v = p.slice(eq + 1).trim();
|
|
546
|
+
if (v.length >= 2 && v[0] === '"' && v[v.length - 1] === '"') v = v.slice(1, -1);
|
|
547
|
+
out[k] = v;
|
|
548
|
+
}
|
|
549
|
+
return out;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async function _parseMultipart(req, opts, ctParams) {
|
|
553
|
+
var boundary = ctParams.boundary;
|
|
554
|
+
if (typeof boundary !== "string" || boundary.length === 0) {
|
|
555
|
+
throw new BodyParserError(
|
|
556
|
+
"body-parser/multipart-no-boundary",
|
|
557
|
+
"multipart Content-Type missing boundary parameter",
|
|
558
|
+
true, HTTP_STATUS.BAD_REQUEST
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
// Resolve tmpDir per-request so directory-creation failure surfaces as a
|
|
562
|
+
// structured error rather than a deferred fs throw.
|
|
563
|
+
var tmpDir = opts.tmpDir || path.join(os.tmpdir(), "blamejs-uploads");
|
|
564
|
+
try { atomicFile.ensureDir(tmpDir, 0o700); }
|
|
565
|
+
catch (e) {
|
|
566
|
+
throw new BodyParserError(
|
|
567
|
+
"body-parser/multipart-tmpdir",
|
|
568
|
+
"could not create multipart tmp dir '" + tmpDir + "': " + ((e && e.message) || String(e)),
|
|
569
|
+
true, 500
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
var boundaryBuf = Buffer.from("--" + boundary);
|
|
574
|
+
var boundaryDelimBuf = Buffer.from("\r\n--" + boundary);
|
|
575
|
+
|
|
576
|
+
var fields = {};
|
|
577
|
+
var files = [];
|
|
578
|
+
var filesRejected = [];
|
|
579
|
+
var totalRead = 0;
|
|
580
|
+
var fileCount = 0;
|
|
581
|
+
var fieldCount = 0;
|
|
582
|
+
var fileSize = opts.fileSize;
|
|
583
|
+
var totalSize = opts.totalSize;
|
|
584
|
+
var fileLimit = opts.fileCount;
|
|
585
|
+
var fieldLimit = opts.fieldCount;
|
|
586
|
+
var fieldSize = opts.fieldSize;
|
|
587
|
+
var mimeAllowlist = Array.isArray(opts.mimeAllowlist) ? opts.mimeAllowlist : null;
|
|
588
|
+
var fileFilter = typeof opts.fileFilter === "function" ? opts.fileFilter : null;
|
|
589
|
+
var perField = (opts.fields && typeof opts.fields === "object") ? opts.fields : null;
|
|
590
|
+
var auditInst = (opts.audit && typeof opts.audit.safeEmit === "function") ? opts.audit : null;
|
|
591
|
+
|
|
592
|
+
var state = MP_INITIAL;
|
|
593
|
+
var pending = Buffer.alloc(0);
|
|
594
|
+
var currentHeaders = null;
|
|
595
|
+
var currentField = null;
|
|
596
|
+
var currentFilename = null;
|
|
597
|
+
var currentMime = null;
|
|
598
|
+
var currentTmpPath = null;
|
|
599
|
+
var currentFd = null;
|
|
600
|
+
var currentSize = 0;
|
|
601
|
+
var currentHash = null;
|
|
602
|
+
var currentBuf = null; // for fields (in-memory accumulator)
|
|
603
|
+
var currentDiscarded = false; // true when fileFilter rejected the part — body bytes are
|
|
604
|
+
// still consumed (we have to read past them to find the next
|
|
605
|
+
// boundary) but never written to disk.
|
|
606
|
+
var currentEffectiveLimit = 0; // per-field-or-global cap; recomputed at part start.
|
|
607
|
+
|
|
608
|
+
function _resetCurrent() {
|
|
609
|
+
currentHeaders = null;
|
|
610
|
+
currentField = null;
|
|
611
|
+
currentFilename = null;
|
|
612
|
+
currentMime = null;
|
|
613
|
+
currentTmpPath = null;
|
|
614
|
+
if (currentFd !== null) { try { fs.closeSync(currentFd); } catch (_e) { /* fd already closed */ } currentFd = null; }
|
|
615
|
+
currentSize = 0;
|
|
616
|
+
currentHash = null;
|
|
617
|
+
currentBuf = null;
|
|
618
|
+
currentDiscarded = false;
|
|
619
|
+
currentEffectiveLimit = 0;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function _emitRejection(field, filename, mimeType, code, message) {
|
|
623
|
+
filesRejected.push({
|
|
624
|
+
field: field,
|
|
625
|
+
filename: filename,
|
|
626
|
+
mimeType: mimeType,
|
|
627
|
+
code: code,
|
|
628
|
+
message: message || null,
|
|
629
|
+
});
|
|
630
|
+
if (auditInst) {
|
|
631
|
+
try {
|
|
632
|
+
auditInst.safeEmit({
|
|
633
|
+
action: "body-parser.multipart.file_rejected",
|
|
634
|
+
outcome: "denied",
|
|
635
|
+
resource: { kind: "multipart.file", id: field + (filename ? ":" + filename : "") },
|
|
636
|
+
metadata: { field: field, filename: filename, mimeType: mimeType, code: code, message: message || null },
|
|
637
|
+
});
|
|
638
|
+
} catch (_e) { /* audit best-effort */ }
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function _cleanup() {
|
|
643
|
+
if (currentFd !== null) { try { fs.closeSync(currentFd); } catch (_e) { /* fd already closed */ } currentFd = null; }
|
|
644
|
+
if (currentTmpPath) { try { fs.unlinkSync(currentTmpPath); } catch (_e) { /* tmp file already removed */ } }
|
|
645
|
+
for (var i = 0; i < files.length; i++) {
|
|
646
|
+
try { fs.unlinkSync(files[i].path); } catch (_e) { /* tmp file already removed */ }
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
try {
|
|
651
|
+
return await new Promise(function (resolve, reject) {
|
|
652
|
+
function done(err, value) {
|
|
653
|
+
// De-dup completion — req error + req end can both fire.
|
|
654
|
+
if (resolved) return;
|
|
655
|
+
resolved = true;
|
|
656
|
+
if (err) {
|
|
657
|
+
_cleanup();
|
|
658
|
+
reject(err);
|
|
659
|
+
} else {
|
|
660
|
+
resolve(value);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
var resolved = false;
|
|
664
|
+
|
|
665
|
+
function processBuffer() {
|
|
666
|
+
// Re-enter the state machine until we can't make progress.
|
|
667
|
+
while (true) {
|
|
668
|
+
if (state === MP_INITIAL) {
|
|
669
|
+
// Find the first boundary marker (without the leading \r\n
|
|
670
|
+
// since the preamble may begin with the boundary directly).
|
|
671
|
+
var firstIdx = pending.indexOf(boundaryBuf);
|
|
672
|
+
if (firstIdx === -1) {
|
|
673
|
+
// Need more data.
|
|
674
|
+
if (pending.length > boundary.length + 100) {
|
|
675
|
+
// Drop preamble bytes to bound memory; keep the last
|
|
676
|
+
// boundary.length+4 bytes as look-ahead.
|
|
677
|
+
pending = pending.slice(pending.length - boundary.length - 4);
|
|
678
|
+
}
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
pending = pending.slice(firstIdx + boundaryBuf.length);
|
|
682
|
+
state = MP_AFTER_BD;
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (state === MP_AFTER_BD) {
|
|
686
|
+
if (pending.length < 2) return;
|
|
687
|
+
if (pending[0] === 0x2d && pending[1] === 0x2d) { // "--"
|
|
688
|
+
state = MP_DONE;
|
|
689
|
+
done(null, { fields: fields, files: files, filesRejected: filesRejected });
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (pending[0] === 0x0d && pending[1] === 0x0a) { // "\r\n"
|
|
693
|
+
pending = pending.slice(2);
|
|
694
|
+
state = MP_HEADERS;
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
// Tolerate transport-added \n only.
|
|
698
|
+
if (pending[0] === 0x0a) {
|
|
699
|
+
pending = pending.slice(1);
|
|
700
|
+
state = MP_HEADERS;
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
done(new BodyParserError("body-parser/multipart-malformed",
|
|
704
|
+
"expected --, \\r\\n, or \\n after boundary", true, HTTP_STATUS.BAD_REQUEST));
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (state === MP_HEADERS) {
|
|
708
|
+
// Read until \r\n\r\n.
|
|
709
|
+
var headEnd = pending.indexOf("\r\n\r\n");
|
|
710
|
+
if (headEnd === -1) {
|
|
711
|
+
if (pending.length > C.BYTES.kib(16)) {
|
|
712
|
+
done(new BodyParserError("body-parser/multipart-headers-too-large",
|
|
713
|
+
"multipart part headers exceed 16KB", true, 413));
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
currentHeaders = _parseMultipartHeaders(pending.slice(0, headEnd).toString("utf8"));
|
|
719
|
+
pending = pending.slice(headEnd + 4);
|
|
720
|
+
// Decode Content-Disposition.
|
|
721
|
+
var cd = _parseHeaderParams(currentHeaders["content-disposition"]);
|
|
722
|
+
if (cd._value !== "form-data" || typeof cd.name !== "string" || cd.name.length === 0) {
|
|
723
|
+
done(new BodyParserError("body-parser/multipart-bad-disposition",
|
|
724
|
+
"multipart part missing form-data Content-Disposition", true, HTTP_STATUS.BAD_REQUEST));
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (POISONED_KEYS.has(cd.name)) {
|
|
728
|
+
done(new BodyParserError("body-parser/multipart-poisoned-field",
|
|
729
|
+
"multipart field '" + cd.name + "' is forbidden (prototype-pollution defense)",
|
|
730
|
+
true, HTTP_STATUS.BAD_REQUEST));
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
currentField = cd.name;
|
|
734
|
+
if (typeof cd.filename === "string") {
|
|
735
|
+
currentFilename = _sanitizeFilename(cd.filename);
|
|
736
|
+
if (!currentFilename) {
|
|
737
|
+
done(new BodyParserError("body-parser/multipart-bad-filename",
|
|
738
|
+
"multipart part filename did not survive sanitization (path traversal or empty)",
|
|
739
|
+
true, HTTP_STATUS.BAD_REQUEST));
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
currentMime = currentHeaders["content-type"] || "application/octet-stream";
|
|
743
|
+
// Per-field MIME allowlist takes precedence over the global one
|
|
744
|
+
// for this field; global applies to fields without an entry.
|
|
745
|
+
var fieldRule = perField ? perField[currentField] : null;
|
|
746
|
+
var perFieldMime = (fieldRule && Array.isArray(fieldRule.mimeTypes))
|
|
747
|
+
? fieldRule.mimeTypes : null;
|
|
748
|
+
if (perFieldMime) {
|
|
749
|
+
if (perFieldMime.indexOf(currentMime) === -1) {
|
|
750
|
+
done(new BodyParserError("body-parser/multipart-mime-not-allowed",
|
|
751
|
+
"multipart file '" + currentField + "' MIME '" + currentMime +
|
|
752
|
+
"' is not on the per-field allowlist",
|
|
753
|
+
true, 415));
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
} else if (mimeAllowlist && mimeAllowlist.indexOf(currentMime) === -1) {
|
|
757
|
+
done(new BodyParserError("body-parser/multipart-mime-not-allowed",
|
|
758
|
+
"multipart file MIME '" + currentMime + "' is not on the allowlist",
|
|
759
|
+
true, 415));
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
fileCount++;
|
|
763
|
+
if (fileCount > fileLimit) {
|
|
764
|
+
done(new BodyParserError("body-parser/multipart-too-many-files",
|
|
765
|
+
"multipart fileCount exceeds limit (" + fileLimit + ")",
|
|
766
|
+
true, 413));
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
// Per-field cap overrides global fileSize for this field.
|
|
770
|
+
currentEffectiveLimit = (fieldRule && typeof fieldRule.maxBytes === "number")
|
|
771
|
+
? fieldRule.maxBytes : fileSize;
|
|
772
|
+
|
|
773
|
+
// fileFilter runs AFTER sanitize + MIME checks but BEFORE the
|
|
774
|
+
// tmp file opens. Synchronous so the parser can decide between
|
|
775
|
+
// disk-write and discard-bytes without buffering the part.
|
|
776
|
+
if (fileFilter) {
|
|
777
|
+
var filterVerdict;
|
|
778
|
+
try {
|
|
779
|
+
filterVerdict = fileFilter({
|
|
780
|
+
field: currentField,
|
|
781
|
+
filename: currentFilename,
|
|
782
|
+
mimeType: currentMime,
|
|
783
|
+
partHeaders: currentHeaders,
|
|
784
|
+
});
|
|
785
|
+
} catch (e) {
|
|
786
|
+
done(new BodyParserError("body-parser/multipart-file-filter-throw",
|
|
787
|
+
"fileFilter threw: " + ((e && e.message) || String(e)),
|
|
788
|
+
true, 500));
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (filterVerdict === false ||
|
|
792
|
+
(filterVerdict && typeof filterVerdict === "object" && filterVerdict.reject)) {
|
|
793
|
+
var rejCode = (filterVerdict && filterVerdict.code) || "fileFilter";
|
|
794
|
+
var rejMessage = (filterVerdict && filterVerdict.message) || null;
|
|
795
|
+
_emitRejection(currentField, currentFilename, currentMime, rejCode, rejMessage);
|
|
796
|
+
// Read past the body bytes (we still must find the next
|
|
797
|
+
// boundary) but never open a tmp file or push to req.files.
|
|
798
|
+
currentDiscarded = true;
|
|
799
|
+
fileCount--; // doesn't count toward the limit since it didn't land
|
|
800
|
+
currentSize = 0;
|
|
801
|
+
state = MP_BODY;
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Generate the tmp path — never derived from the
|
|
807
|
+
// operator-supplied filename.
|
|
808
|
+
var unique = crypto.generateToken(C.BYTES.bytes(16));
|
|
809
|
+
currentTmpPath = path.join(tmpDir, "blamejs-up-" + unique);
|
|
810
|
+
try {
|
|
811
|
+
currentFd = fs.openSync(currentTmpPath, "wx", 0o600);
|
|
812
|
+
} catch (e) {
|
|
813
|
+
done(new BodyParserError("body-parser/multipart-tmp-open",
|
|
814
|
+
"could not open multipart tmp file: " + ((e && e.message) || String(e)),
|
|
815
|
+
true, 500));
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
currentHash = nodeCrypto.createHash("sha3-512");
|
|
819
|
+
currentSize = 0;
|
|
820
|
+
} else {
|
|
821
|
+
fieldCount++;
|
|
822
|
+
if (fieldCount > fieldLimit) {
|
|
823
|
+
done(new BodyParserError("body-parser/multipart-too-many-fields",
|
|
824
|
+
"multipart fieldCount exceeds limit (" + fieldLimit + ")",
|
|
825
|
+
true, 413));
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
// Per-field cap overrides global fieldSize for text parts too.
|
|
829
|
+
var textFieldRule = perField ? perField[currentField] : null;
|
|
830
|
+
currentEffectiveLimit = (textFieldRule && typeof textFieldRule.maxBytes === "number")
|
|
831
|
+
? textFieldRule.maxBytes : fieldSize;
|
|
832
|
+
currentBuf = [];
|
|
833
|
+
currentSize = 0;
|
|
834
|
+
}
|
|
835
|
+
state = MP_BODY;
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
if (state === MP_BODY) {
|
|
839
|
+
// Look for the next boundary. The marker we want is
|
|
840
|
+
// \r\n--<boundary>
|
|
841
|
+
// Anything before it is part body. We need to keep at least
|
|
842
|
+
// boundary.length + 4 bytes in `pending` so we don't emit a
|
|
843
|
+
// partial match as body bytes.
|
|
844
|
+
var bdIdx = pending.indexOf(boundaryDelimBuf);
|
|
845
|
+
var emitLen;
|
|
846
|
+
if (bdIdx === -1) {
|
|
847
|
+
// No marker yet — emit everything except the trailing
|
|
848
|
+
// boundary-length+4 bytes (might be the start of the marker).
|
|
849
|
+
if (pending.length <= boundaryDelimBuf.length) return;
|
|
850
|
+
emitLen = pending.length - boundaryDelimBuf.length;
|
|
851
|
+
} else {
|
|
852
|
+
emitLen = bdIdx;
|
|
853
|
+
}
|
|
854
|
+
if (emitLen > 0) {
|
|
855
|
+
var bodyChunk = pending.slice(0, emitLen);
|
|
856
|
+
if (currentDiscarded) {
|
|
857
|
+
// fileFilter rejected this part — read past the bytes to find
|
|
858
|
+
// the next boundary but never write to disk. totalSize still
|
|
859
|
+
// applies as a per-request DoS guard.
|
|
860
|
+
totalRead += bodyChunk.length;
|
|
861
|
+
if (totalRead > totalSize) {
|
|
862
|
+
done(new BodyParserError("body-parser/multipart-total-too-large",
|
|
863
|
+
"multipart total request size exceeds totalSize (" + totalSize + ")",
|
|
864
|
+
true, 413));
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
} else if (currentFd !== null) {
|
|
868
|
+
// File part — write to disk.
|
|
869
|
+
currentSize += bodyChunk.length;
|
|
870
|
+
if (currentSize > currentEffectiveLimit) {
|
|
871
|
+
var perFieldFile = (perField && perField[currentField] &&
|
|
872
|
+
typeof perField[currentField].maxBytes === "number");
|
|
873
|
+
done(new BodyParserError("body-parser/multipart-file-too-large",
|
|
874
|
+
"multipart file '" + currentField + "' exceeds " +
|
|
875
|
+
(perFieldFile ? "per-field maxBytes" : "fileSize") +
|
|
876
|
+
" (" + currentEffectiveLimit + ")",
|
|
877
|
+
true, 413));
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
totalRead += bodyChunk.length;
|
|
881
|
+
if (totalRead > totalSize) {
|
|
882
|
+
done(new BodyParserError("body-parser/multipart-total-too-large",
|
|
883
|
+
"multipart total request size exceeds totalSize (" + totalSize + ")",
|
|
884
|
+
true, 413));
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
try {
|
|
888
|
+
var written = 0;
|
|
889
|
+
while (written < bodyChunk.length) {
|
|
890
|
+
written += fs.writeSync(currentFd, bodyChunk, written, bodyChunk.length - written);
|
|
891
|
+
}
|
|
892
|
+
} catch (e) {
|
|
893
|
+
done(new BodyParserError("body-parser/multipart-tmp-write",
|
|
894
|
+
"multipart tmp write failed: " + ((e && e.message) || String(e)),
|
|
895
|
+
true, 500));
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
currentHash.update(bodyChunk);
|
|
899
|
+
} else {
|
|
900
|
+
// Field part — buffer in memory up to per-field-or-global cap.
|
|
901
|
+
currentSize += bodyChunk.length;
|
|
902
|
+
if (currentSize > currentEffectiveLimit) {
|
|
903
|
+
var perFieldText = (perField && perField[currentField] &&
|
|
904
|
+
typeof perField[currentField].maxBytes === "number");
|
|
905
|
+
done(new BodyParserError("body-parser/multipart-field-too-large",
|
|
906
|
+
"multipart field '" + currentField + "' exceeds " +
|
|
907
|
+
(perFieldText ? "per-field maxBytes" : "fieldSize") +
|
|
908
|
+
" (" + currentEffectiveLimit + ")",
|
|
909
|
+
true, 413));
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
totalRead += bodyChunk.length;
|
|
913
|
+
if (totalRead > totalSize) {
|
|
914
|
+
done(new BodyParserError("body-parser/multipart-total-too-large",
|
|
915
|
+
"multipart total request size exceeds totalSize (" + totalSize + ")",
|
|
916
|
+
true, 413));
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
currentBuf.push(bodyChunk);
|
|
920
|
+
}
|
|
921
|
+
pending = pending.slice(emitLen);
|
|
922
|
+
}
|
|
923
|
+
if (bdIdx === -1) return; // need more data
|
|
924
|
+
// Consume the boundary delimiter; transition to AFTER_BD.
|
|
925
|
+
pending = pending.slice(boundaryDelimBuf.length);
|
|
926
|
+
// Finalize the current part.
|
|
927
|
+
if (currentDiscarded) {
|
|
928
|
+
// fileFilter rejected — already recorded in filesRejected; no
|
|
929
|
+
// tmp file was opened, nothing to clean up here.
|
|
930
|
+
} else if (currentFd !== null) {
|
|
931
|
+
try { fs.closeSync(currentFd); } catch (_e) { /* fd already closed */ }
|
|
932
|
+
currentFd = null;
|
|
933
|
+
files.push({
|
|
934
|
+
field: currentField,
|
|
935
|
+
filename: currentFilename,
|
|
936
|
+
mimeType: currentMime,
|
|
937
|
+
path: currentTmpPath,
|
|
938
|
+
size: currentSize,
|
|
939
|
+
hash: currentHash.digest("hex"),
|
|
940
|
+
});
|
|
941
|
+
} else {
|
|
942
|
+
// Field part — flatten + decode UTF-8.
|
|
943
|
+
var fbuf = Buffer.concat(currentBuf);
|
|
944
|
+
var text = fbuf.toString("utf8");
|
|
945
|
+
// Repeated field name → array, matching urlencoded parser.
|
|
946
|
+
if (Object.prototype.hasOwnProperty.call(fields, currentField)) {
|
|
947
|
+
if (Array.isArray(fields[currentField])) fields[currentField].push(text);
|
|
948
|
+
else fields[currentField] = [fields[currentField], text];
|
|
949
|
+
} else {
|
|
950
|
+
fields[currentField] = text;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
currentHeaders = null;
|
|
954
|
+
currentField = null;
|
|
955
|
+
currentFilename = null;
|
|
956
|
+
currentMime = null;
|
|
957
|
+
currentTmpPath = null;
|
|
958
|
+
currentSize = 0;
|
|
959
|
+
currentHash = null;
|
|
960
|
+
currentBuf = null;
|
|
961
|
+
currentDiscarded = false;
|
|
962
|
+
currentEffectiveLimit = 0;
|
|
963
|
+
state = MP_AFTER_BD;
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
if (state === MP_DONE) return;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
req.on("data", function (chunk) {
|
|
971
|
+
if (resolved) return;
|
|
972
|
+
pending = Buffer.concat([pending, chunk]);
|
|
973
|
+
try { processBuffer(); }
|
|
974
|
+
catch (e) {
|
|
975
|
+
done(new BodyParserError("body-parser/multipart-internal",
|
|
976
|
+
"multipart internal parse error: " + ((e && e.message) || String(e)),
|
|
977
|
+
true, 500));
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
req.on("end", function () {
|
|
981
|
+
if (resolved) return;
|
|
982
|
+
if (state !== MP_DONE) {
|
|
983
|
+
done(new BodyParserError("body-parser/multipart-truncated",
|
|
984
|
+
"multipart stream ended before final boundary", true, HTTP_STATUS.BAD_REQUEST));
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
req.on("error", function (e) {
|
|
989
|
+
if (resolved) return;
|
|
990
|
+
done(new BodyParserError("body-parser/multipart-stream",
|
|
991
|
+
"multipart stream error: " + ((e && e.message) || String(e)), true, HTTP_STATUS.BAD_REQUEST));
|
|
992
|
+
});
|
|
993
|
+
});
|
|
994
|
+
} catch (e) {
|
|
995
|
+
// Already cleaned up via done(err).
|
|
996
|
+
throw e;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// ---- main middleware factory ----
|
|
1001
|
+
|
|
1002
|
+
function create(opts) {
|
|
1003
|
+
opts = opts || {};
|
|
1004
|
+
validateOpts(opts, [
|
|
1005
|
+
"json", "urlencoded", "text", "raw", "multipart", "keepRawBody",
|
|
1006
|
+
], "middleware.bodyParser");
|
|
1007
|
+
|
|
1008
|
+
function _resolve(name) {
|
|
1009
|
+
if (opts[name] === false) return null; // disabled
|
|
1010
|
+
return Object.assign({}, DEFAULTS[name], opts[name] || {});
|
|
1011
|
+
}
|
|
1012
|
+
var jsonOpts = _resolve("json");
|
|
1013
|
+
var urlencodedOpts = _resolve("urlencoded");
|
|
1014
|
+
var textOpts = _resolve("text");
|
|
1015
|
+
var rawOpts = _resolve("raw");
|
|
1016
|
+
var multipartOpts = _resolve("multipart");
|
|
1017
|
+
var keepRawBody = !!opts.keepRawBody;
|
|
1018
|
+
|
|
1019
|
+
return async function bodyParser(req, res, next) {
|
|
1020
|
+
// RFC 9112 §6.1 request-smuggling defense — runs BEFORE _hasBody
|
|
1021
|
+
// so the smuggling shape is rejected even if the request would
|
|
1022
|
+
// otherwise short-circuit as no-body. Reject with 400 +
|
|
1023
|
+
// Connection: close so the upstream proxy doesn't reuse the socket.
|
|
1024
|
+
var smug = _detectSmuggling(req);
|
|
1025
|
+
if (smug) {
|
|
1026
|
+
if (!res.headersSent) {
|
|
1027
|
+
var smugBody = JSON.stringify({ error: smug.message, code: smug.code });
|
|
1028
|
+
res.writeHead(smug.status, {
|
|
1029
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
1030
|
+
"Content-Length": Buffer.byteLength(smugBody),
|
|
1031
|
+
"Connection": "close",
|
|
1032
|
+
});
|
|
1033
|
+
res.end(smugBody);
|
|
1034
|
+
}
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (!_hasBody(req)) return next();
|
|
1038
|
+
if (req.body !== undefined) return next(); // already parsed by an earlier middleware
|
|
1039
|
+
|
|
1040
|
+
var ct = _contentType(req);
|
|
1041
|
+
|
|
1042
|
+
try {
|
|
1043
|
+
if (jsonOpts && _typeMatches(ct.type, jsonOpts.contentTypes)) {
|
|
1044
|
+
if (keepRawBody) {
|
|
1045
|
+
var rawBuf = await _bufferBody(req, jsonOpts.limit);
|
|
1046
|
+
req.bodyRaw = rawBuf;
|
|
1047
|
+
req.body = rawBuf.length === 0 ? undefined : await _parseJsonFromBuf(rawBuf, jsonOpts);
|
|
1048
|
+
} else {
|
|
1049
|
+
req.body = await _parseJson(req, jsonOpts);
|
|
1050
|
+
}
|
|
1051
|
+
return next();
|
|
1052
|
+
}
|
|
1053
|
+
if (urlencodedOpts && _typeMatches(ct.type, urlencodedOpts.contentTypes)) {
|
|
1054
|
+
req.body = await _parseUrlencoded(req, urlencodedOpts);
|
|
1055
|
+
return next();
|
|
1056
|
+
}
|
|
1057
|
+
if (multipartOpts && _typeMatches(ct.type, multipartOpts.contentTypes)) {
|
|
1058
|
+
var mpResult = await _parseMultipart(req, multipartOpts, ct.params);
|
|
1059
|
+
req.body = mpResult.fields;
|
|
1060
|
+
req.files = mpResult.files;
|
|
1061
|
+
req.filesRejected = mpResult.filesRejected || [];
|
|
1062
|
+
// Cleanup tmp files when the response finishes / closes / errors,
|
|
1063
|
+
// regardless of whether the handler returned cleanly. Operators
|
|
1064
|
+
// who want to KEEP a file move it elsewhere inside the handler.
|
|
1065
|
+
var cleanedUp = false;
|
|
1066
|
+
function cleanup() {
|
|
1067
|
+
if (cleanedUp) return;
|
|
1068
|
+
cleanedUp = true;
|
|
1069
|
+
for (var i = 0; i < mpResult.files.length; i++) {
|
|
1070
|
+
try { fs.unlinkSync(mpResult.files[i].path); } catch (_e) { /* tmp file already removed */ }
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
res.on("finish", cleanup);
|
|
1074
|
+
res.on("close", cleanup);
|
|
1075
|
+
return next();
|
|
1076
|
+
}
|
|
1077
|
+
if (textOpts && _typeMatches(ct.type, textOpts.contentTypes)) {
|
|
1078
|
+
req.body = await _parseText(req, textOpts);
|
|
1079
|
+
return next();
|
|
1080
|
+
}
|
|
1081
|
+
if (rawOpts && _typeMatches(ct.type, rawOpts.contentTypes)) {
|
|
1082
|
+
req.body = await _parseRaw(req, rawOpts);
|
|
1083
|
+
return next();
|
|
1084
|
+
}
|
|
1085
|
+
// Body present but no enabled sub-parser matches the Content-Type.
|
|
1086
|
+
// Don't reject silently — operators with a custom parser opted in
|
|
1087
|
+
// by NOT enabling any sub-parser for this Content-Type would skip
|
|
1088
|
+
// bodyParser entirely. Reaching here means: enabled parsers exist
|
|
1089
|
+
// but none match this body. 415 is honest about the mismatch.
|
|
1090
|
+
_writeError(res, 415,
|
|
1091
|
+
"Unsupported Content-Type '" + ct.type + "'. Enable a matching sub-parser or send a different type.",
|
|
1092
|
+
"body-parser/unsupported-content-type"
|
|
1093
|
+
);
|
|
1094
|
+
} catch (e) {
|
|
1095
|
+
var status = (e && typeof e.statusCode === "number") ? e.statusCode : HTTP_STATUS.BAD_REQUEST;
|
|
1096
|
+
var code = (e && typeof e.code === "string") ? e.code : "body-parser/error";
|
|
1097
|
+
var message = (e && e.message) ? e.message : String(e);
|
|
1098
|
+
_writeError(res, status, message, code);
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// Helper used by keepRawBody path — re-uses the JSON pipeline but
|
|
1104
|
+
// works from an already-buffered Buffer (so we don't read req twice).
|
|
1105
|
+
async function _parseJsonFromBuf(buf, opts) {
|
|
1106
|
+
var text = buf.toString(opts.charset);
|
|
1107
|
+
if (opts.strict) {
|
|
1108
|
+
var head = text.replace(/^[\s\u00A0\uFEFF]+/, "")[0];
|
|
1109
|
+
if (head !== "{" && head !== "[") {
|
|
1110
|
+
throw new BodyParserError("body-parser/json-strict",
|
|
1111
|
+
"JSON body must start with '{' or '[' (strict mode)", true, HTTP_STATUS.BAD_REQUEST);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
var parsed;
|
|
1115
|
+
try { parsed = safeJson.parse(text, { maxBytes: opts.limit }); }
|
|
1116
|
+
catch (e) {
|
|
1117
|
+
throw new BodyParserError("body-parser/json-malformed",
|
|
1118
|
+
"JSON parse failed: " + ((e && e.message) || String(e)), true, HTTP_STATUS.BAD_REQUEST);
|
|
1119
|
+
}
|
|
1120
|
+
if (typeof opts.parseHook === "function") {
|
|
1121
|
+
try { parsed = opts.parseHook(parsed); }
|
|
1122
|
+
catch (e) {
|
|
1123
|
+
throw new BodyParserError("body-parser/json-hook",
|
|
1124
|
+
"JSON parseHook failed: " + ((e && e.message) || String(e)), true, HTTP_STATUS.BAD_REQUEST);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return parsed;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// raw — convenience wrapper that returns a middleware which buffers
|
|
1131
|
+
// the request body as a Buffer regardless of Content-Type. Webhook
|
|
1132
|
+
// signature-verification routes use this — the HMAC is computed over
|
|
1133
|
+
// the literal body bytes, so JSON-parsing first would change them.
|
|
1134
|
+
//
|
|
1135
|
+
// router.post("/hooks/in", b.middleware.bodyParser.raw(), function (req, res) {
|
|
1136
|
+
// verifier.verify(req.headers["x-signature"], req.body); // req.body is a Buffer
|
|
1137
|
+
// });
|
|
1138
|
+
//
|
|
1139
|
+
// Accepts the same `raw`-section opts as create() (limit, contentTypes).
|
|
1140
|
+
// contentTypes default expands to `["*/*"]` so any Content-Type lands
|
|
1141
|
+
// as raw bytes.
|
|
1142
|
+
function raw(opts) {
|
|
1143
|
+
opts = opts || {};
|
|
1144
|
+
return create({
|
|
1145
|
+
json: false,
|
|
1146
|
+
urlencoded: false,
|
|
1147
|
+
text: false,
|
|
1148
|
+
multipart: false,
|
|
1149
|
+
raw: {
|
|
1150
|
+
limit: opts.limit != null ? opts.limit : DEFAULTS.raw.limit,
|
|
1151
|
+
contentTypes: opts.contentTypes || ["*/*"],
|
|
1152
|
+
},
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Attach raw onto create so b.middleware.bodyParser.raw() works
|
|
1157
|
+
// (middleware/index.js exports the create function as the namespace
|
|
1158
|
+
// itself, so static helpers hang off it).
|
|
1159
|
+
create.raw = raw;
|
|
1160
|
+
|
|
1161
|
+
module.exports = {
|
|
1162
|
+
create: create,
|
|
1163
|
+
raw: raw,
|
|
1164
|
+
BodyParserError: BodyParserError,
|
|
1165
|
+
// Internal helpers exposed for tests + the csrf-protect refactor.
|
|
1166
|
+
_contentType: _contentType,
|
|
1167
|
+
_hasBody: _hasBody,
|
|
1168
|
+
_sanitizeFilename: _sanitizeFilename,
|
|
1169
|
+
POISONED_KEYS: POISONED_KEYS,
|
|
1170
|
+
};
|