@blamejs/blamejs-shop 0.1.38 → 0.2.0
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 +4 -0
- package/lib/asset-manifest.json +3 -3
- package/lib/storefront.js +94 -21
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +8 -0
- package/lib/vendor/blamejs/README.md +1 -1
- package/lib/vendor/blamejs/SECURITY.md +1 -0
- package/lib/vendor/blamejs/api-snapshot.json +2 -2
- package/lib/vendor/blamejs/lib/archive-read.js +17 -0
- package/lib/vendor/blamejs/lib/auth/fal.js +12 -0
- package/lib/vendor/blamejs/lib/auth/jwt-external.js +15 -11
- package/lib/vendor/blamejs/lib/auth/jwt.js +2 -2
- package/lib/vendor/blamejs/lib/auth/oauth.js +7 -6
- package/lib/vendor/blamejs/lib/auth/oid4vci.js +3 -3
- package/lib/vendor/blamejs/lib/auth/saml.js +15 -12
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +3 -3
- package/lib/vendor/blamejs/lib/calendar.js +9 -2
- package/lib/vendor/blamejs/lib/circuit-breaker.js +5 -4
- package/lib/vendor/blamejs/lib/crypto-hpke.js +1 -1
- package/lib/vendor/blamejs/lib/crypto-oprf.js +2 -2
- package/lib/vendor/blamejs/lib/crypto.js +2 -2
- package/lib/vendor/blamejs/lib/framework-error.js +2 -1
- package/lib/vendor/blamejs/lib/guard-jwt.js +3 -2
- package/lib/vendor/blamejs/lib/guard-smtp-command.js +2 -2
- package/lib/vendor/blamejs/lib/mail-auth.js +2 -2
- package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +1 -1
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +7 -7
- package/lib/vendor/blamejs/lib/mail-crypto.js +1 -1
- package/lib/vendor/blamejs/lib/mail-dav.js +5 -4
- package/lib/vendor/blamejs/lib/mail-deploy.js +3 -2
- package/lib/vendor/blamejs/lib/mail-server-imap.js +1 -1
- package/lib/vendor/blamejs/lib/mail-server-jmap.js +1 -1
- package/lib/vendor/blamejs/lib/mail-server-managesieve.js +1 -1
- package/lib/vendor/blamejs/lib/mail-server-mx.js +142 -47
- package/lib/vendor/blamejs/lib/mail-server-submission.js +3 -3
- package/lib/vendor/blamejs/lib/mail-store.js +2 -2
- package/lib/vendor/blamejs/lib/mail.js +2 -2
- package/lib/vendor/blamejs/lib/network-tls.js +10 -7
- package/lib/vendor/blamejs/lib/safe-decompress.js +8 -6
- package/lib/vendor/blamejs/lib/safe-ical.js +12 -12
- package/lib/vendor/blamejs/lib/safe-mime.js +6 -6
- package/lib/vendor/blamejs/lib/safe-sieve.js +1 -1
- package/lib/vendor/blamejs/lib/safe-smtp.js +1 -1
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.13.10.json +44 -0
- package/lib/vendor/blamejs/release-notes/v0.13.11.json +27 -0
- package/lib/vendor/blamejs/release-notes/v0.13.12.json +36 -0
- package/lib/vendor/blamejs/release-notes/v0.13.9.json +27 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +32 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +4 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/calendar.test.js +31 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +100 -7
- package/lib/vendor/blamejs/test/layer-0-primitives/fal.test.js +26 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime.test.js +5 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-dav.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +166 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-cluster.test.js +25 -18
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-ical.test.js +2 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/sandbox.test.js +12 -12
- package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-exactly-once.test.js +15 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/websocket-channels.test.js +0 -6
- package/package.json +1 -1
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*
|
|
26
26
|
* ## Defenses baked in
|
|
27
27
|
*
|
|
28
|
-
* - **SMTP smuggling** (CVE-2023-51764 / CVE-
|
|
28
|
+
* - **SMTP smuggling** (CVE-2023-51764 Postfix / CVE-2023-51765 Sendmail / CVE-2023-51766 Exim) — every
|
|
29
29
|
* wire line passes through `b.guardSmtpCommand.validate` which
|
|
30
30
|
* refuses bare LF, bare CR, NUL, C0 controls, DEL, and oversize.
|
|
31
31
|
* The DATA body's `\r\n.\r\n` terminator is matched on canonical
|
|
@@ -67,9 +67,11 @@
|
|
|
67
67
|
*
|
|
68
68
|
* - `mail.server.mx.connect` — IP, TLS state, FCrDNS hostname
|
|
69
69
|
* - `mail.server.mx.helo` — HELO greeting, helo-gate verdict
|
|
70
|
-
* - `mail.server.mx.
|
|
71
|
-
* - `mail.server.mx.
|
|
72
|
-
* - `mail.server.mx.
|
|
70
|
+
* - `mail.server.mx.helo_gate_refused` — HELO identity refused (gate action)
|
|
71
|
+
* - `mail.server.mx.mail_from` — sender address
|
|
72
|
+
* - `mail.server.mx.rcpt_to` — recipient, rblListed flag, greylist action
|
|
73
|
+
* - `mail.server.mx.rbl_refused` — connecting IP on a DNS blocklist (zones)
|
|
74
|
+
* - `mail.server.mx.greylist_deferred` — (ip, from, rcpt) first-seen 450 deferral
|
|
73
75
|
* - `mail.server.mx.data_refused` — refusal reason + SMTP code (5xx vs 4xx)
|
|
74
76
|
* - `mail.server.mx.delivered` — agent.handoff ack
|
|
75
77
|
* - `mail.server.mx.tls_handshake_failed` — handshake error
|
|
@@ -100,19 +102,30 @@
|
|
|
100
102
|
*
|
|
101
103
|
* Every gate is a primitive that already exists. The MX slice is a
|
|
102
104
|
* state-machine + wire-protocol coordinator — no new crypto, no
|
|
103
|
-
* new parsing, no new RFC-layer primitives.
|
|
104
|
-
* (e.g.
|
|
105
|
-
* skips that phase
|
|
106
|
-
*
|
|
105
|
+
* new parsing, no new RFC-layer primitives. When the operator
|
|
106
|
+
* doesn't wire a gate (e.g. omits `opts.greylist`), the listener
|
|
107
|
+
* skips that phase rather than synthesizing a verdict.
|
|
108
|
+
*
|
|
109
|
+
* Connection-level gates are wired into the live state machine:
|
|
110
|
+
* `opts.helo` (HELO identity) evaluates at HELO/EHLO; `opts.rbl`
|
|
111
|
+
* (connecting-IP DNS blocklist, evaluated once per connection) and
|
|
112
|
+
* `opts.greylist` ((ip, from, rcpt) first-seen deferral) evaluate at
|
|
113
|
+
* RCPT TO and surface their verdicts on the `rcpt_to` event. The
|
|
114
|
+
* message-authentication gates (`b.guardEnvelope` SPF/DKIM/DMARC
|
|
115
|
+
* alignment) require the inbound SPF + DKIM verification results as
|
|
116
|
+
* inputs; that inbound-auth pipeline composes `b.mail.spf` +
|
|
117
|
+
* `b.mail.dmarc` + DKIM verification and lands as a follow-up, at
|
|
118
|
+
* which point the DATA-phase envelope/DMARC gate wires in. Until
|
|
119
|
+
* then operators run those checks on the delivered message via the
|
|
120
|
+
* agent handoff.
|
|
107
121
|
*
|
|
108
122
|
* @card
|
|
109
123
|
* Inbound SMTP / MX listener. RFC 5321 state machine with SMTP-
|
|
110
124
|
* smuggling defense baked into the wire-protocol layer (RFC 5321
|
|
111
|
-
* §2.3.8 + CVE-2023-51764 /
|
|
125
|
+
* §2.3.8 + CVE-2023-51764 / 51765 / 51766), open-relay refusal by
|
|
112
126
|
* default, STARTTLS-stripping defense (CVE-2021-38371), and the
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* appropriate phase.
|
|
127
|
+
* connection-level gate cascade (HELO identity / RBL / greylist)
|
|
128
|
+
* running at the appropriate phase.
|
|
116
129
|
*/
|
|
117
130
|
|
|
118
131
|
var net = require("node:net");
|
|
@@ -149,6 +162,7 @@ var REPLY_221_BYE = "221";
|
|
|
149
162
|
var REPLY_250_OK = "250";
|
|
150
163
|
var REPLY_354_START_INPUT = "354";
|
|
151
164
|
var REPLY_421_SERVICE_NOT_AVAIL = "421"; // allow:raw-byte-literal — SMTP transient code
|
|
165
|
+
var REPLY_450_MAILBOX_BUSY = "450"; // allow:raw-byte-literal — SMTP transient code (greylist tempfail)
|
|
152
166
|
var REPLY_451_LOCAL_ERROR = "451"; // allow:raw-byte-literal — SMTP transient code
|
|
153
167
|
var REPLY_452_INSUFFICIENT_STG = "452"; // allow:raw-byte-literal — SMTP transient code
|
|
154
168
|
var REPLY_500_SYNTAX = "500"; // allow:raw-byte-literal — SMTP permanent code
|
|
@@ -177,11 +191,9 @@ var RE_SIZE = /SIZE=(\d+)/i;
|
|
|
177
191
|
* @opts
|
|
178
192
|
* tlsContext: TlsContext, // required — b.network.tls.context() output (no implicit plaintext)
|
|
179
193
|
* greeting: string, // default "blamejs ESMTP" — HELO/EHLO 220-line banner
|
|
180
|
-
* helo: b.mail.helo,
|
|
181
|
-
* rbl: b.mail.rbl,
|
|
182
|
-
* greylist: b.mail.greylist, // optional gate
|
|
183
|
-
* envelope: b.guardEnvelope, // optional gate (SPF/DKIM alignment)
|
|
184
|
-
* dmarc: b.mail.auth.dmarc, // optional gate
|
|
194
|
+
* helo: b.mail.helo, // optional gate — HELO identity (FCrDNS / shape / self-name)
|
|
195
|
+
* rbl: b.mail.rbl.create(…), // optional gate — DNS blocklist on the connecting IP
|
|
196
|
+
* greylist: b.mail.greylist.create(…), // optional gate — defer first-seen (ip, from, rcpt)
|
|
185
197
|
* agent: b.mail.agent, // optional delivery handoff
|
|
186
198
|
* relayAllowedFor: [{ cidr, scope }], // operator-explicit relay allowlist; default [] = MX-only
|
|
187
199
|
* localDomains: [string], // RCPT TO local-domain allowlist (refuse non-local with 550 5.7.1)
|
|
@@ -199,7 +211,6 @@ var RE_SIZE = /SIZE=(\d+)/i;
|
|
|
199
211
|
* helo: b.mail.helo,
|
|
200
212
|
* rbl: b.mail.rbl.create({ providers: ["zen.spamhaus.org"] }),
|
|
201
213
|
* greylist: b.mail.greylist.create({ store: greylistStore }),
|
|
202
|
-
* envelope: b.guardEnvelope,
|
|
203
214
|
* agent: b.mail.agent.create({ store: mailStore }),
|
|
204
215
|
* localDomains: ["example.com"],
|
|
205
216
|
* });
|
|
@@ -260,8 +271,8 @@ function create(opts) {
|
|
|
260
271
|
|
|
261
272
|
// Default-on operator-supplied-domain hardening. opts.localDomains
|
|
262
273
|
// and the HELO / MAIL FROM / RCPT TO domain validations all route
|
|
263
|
-
// through `b.guardDomain` for IDN homograph
|
|
264
|
-
// class), special-use-domain refusal (RFC 6761), label-length cap
|
|
274
|
+
// through `b.guardDomain` for IDN homograph / Punycode-spoof defense
|
|
275
|
+
// (mixed-script confusable class), special-use-domain refusal (RFC 6761), label-length cap
|
|
265
276
|
// (RFC 1035 §2.3.4), and bare-IP-as-domain refusal (CVE-2021-22931
|
|
266
277
|
// class). Operators with a closed-network deployment can pass
|
|
267
278
|
// `guardDomain: false` to skip; the default keeps the protection on.
|
|
@@ -367,6 +378,15 @@ function create(opts) {
|
|
|
367
378
|
var lineBuffer = Buffer.alloc(0);
|
|
368
379
|
var bodyCollector = null;
|
|
369
380
|
var inDataBody = false;
|
|
381
|
+
// Async command pump: gates (HELO / RBL / greylist / envelope /
|
|
382
|
+
// DMARC) may await DNS or a store, so command handling is async.
|
|
383
|
+
// `pumpChain` FIFO-serializes per-chunk processing so a gate
|
|
384
|
+
// resolving cannot let a later pipelined command (RFC 2920) jump
|
|
385
|
+
// ahead of an earlier one — reply ordering + the per-command
|
|
386
|
+
// smuggling defenses stay intact. `connClosed` short-circuits any
|
|
387
|
+
// chunk queued before a teardown.
|
|
388
|
+
var pumpChain = Promise.resolve();
|
|
389
|
+
var connClosed = false;
|
|
370
390
|
|
|
371
391
|
socket.setTimeout(idleTimeoutMs);
|
|
372
392
|
socket.on("timeout", function () {
|
|
@@ -382,6 +402,7 @@ function create(opts) {
|
|
|
382
402
|
});
|
|
383
403
|
|
|
384
404
|
socket.on("close", function () {
|
|
405
|
+
connClosed = true;
|
|
385
406
|
connections.delete(socket);
|
|
386
407
|
});
|
|
387
408
|
|
|
@@ -395,20 +416,35 @@ function create(opts) {
|
|
|
395
416
|
// 220 banner — RFC 5321 §3.1.
|
|
396
417
|
_writeReply(socket, REPLY_220_READY, greeting + " ready");
|
|
397
418
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
419
|
+
// Feed a chunk into the per-connection command pump. Chains each
|
|
420
|
+
// chunk behind the previous one's full (async) processing so command
|
|
421
|
+
// handlers + their gates run strictly in arrival order. Used by BOTH
|
|
422
|
+
// the plaintext `socket.on("data")` path AND the post-STARTTLS
|
|
423
|
+
// TLSSocket onData path — otherwise gate awaits on the upgraded
|
|
424
|
+
// socket would overlap later TLS chunks (the default strict/balanced
|
|
425
|
+
// profiles require STARTTLS before MAIL, so the gates run there) and
|
|
426
|
+
// async gate rejections would go unhandled instead of producing the
|
|
427
|
+
// 421 path. `activeSock` is whichever socket is current (plaintext or
|
|
428
|
+
// TLS) so the 421/close lands on the right transport.
|
|
429
|
+
function _feedChunk(activeSock, chunk) {
|
|
430
|
+
pumpChain = pumpChain.then(function () {
|
|
431
|
+
if (connClosed) return undefined;
|
|
432
|
+
return _ingestBytes(state, activeSock, chunk);
|
|
433
|
+
}).catch(function (err) {
|
|
434
|
+
if (connClosed) return;
|
|
401
435
|
_emit("mail.server.mx.handler_threw",
|
|
402
436
|
{ connectionId: state.id, error: (err && err.message) || String(err) },
|
|
403
437
|
"failure");
|
|
404
|
-
try { _writeReply(
|
|
438
|
+
try { _writeReply(activeSock, REPLY_421_SERVICE_NOT_AVAIL, "4.3.0 Server error"); }
|
|
405
439
|
catch (_e) { /* socket already gone */ }
|
|
406
|
-
_closeConnection(
|
|
407
|
-
}
|
|
408
|
-
}
|
|
440
|
+
_closeConnection(activeSock);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
socket.on("data", function (chunk) { _feedChunk(socket, chunk); });
|
|
409
445
|
|
|
410
446
|
// ---- Byte-level ingestion --------------------------------------------
|
|
411
|
-
function _ingestBytes(state, socket, chunk) {
|
|
447
|
+
async function _ingestBytes(state, socket, chunk) {
|
|
412
448
|
if (inDataBody) {
|
|
413
449
|
// DATA body — accumulate via boundedChunkCollector, watch for
|
|
414
450
|
// canonical "\r\n.\r\n" terminator only. Bare-LF dot terminator
|
|
@@ -444,9 +480,9 @@ function create(opts) {
|
|
|
444
480
|
var endIdx = safeSmtp.findDotTerminator(collected);
|
|
445
481
|
if (endIdx !== -1) {
|
|
446
482
|
var body = collected.subarray(0, endIdx);
|
|
447
|
-
_finalizeDataBody(state, socket, body);
|
|
448
483
|
inDataBody = false;
|
|
449
484
|
bodyCollector = null;
|
|
485
|
+
await _finalizeDataBody(state, socket, body);
|
|
450
486
|
}
|
|
451
487
|
return;
|
|
452
488
|
}
|
|
@@ -464,12 +500,13 @@ function create(opts) {
|
|
|
464
500
|
while ((crlf = lineBuffer.indexOf(crlfNeedle)) !== -1) {
|
|
465
501
|
var line = lineBuffer.subarray(0, crlf).toString("utf8");
|
|
466
502
|
lineBuffer = lineBuffer.subarray(crlf + 2);
|
|
467
|
-
_handleCommand(state, socket, line);
|
|
503
|
+
await _handleCommand(state, socket, line);
|
|
468
504
|
if (inDataBody) return;
|
|
505
|
+
if (connClosed) return;
|
|
469
506
|
}
|
|
470
507
|
}
|
|
471
508
|
|
|
472
|
-
function _handleCommand(state, socket, line) {
|
|
509
|
+
async function _handleCommand(state, socket, line) {
|
|
473
510
|
// Per-line guard — refuse bare LF / NUL / C0 / DEL / oversize
|
|
474
511
|
// BEFORE state-machine dispatch.
|
|
475
512
|
try {
|
|
@@ -494,16 +531,16 @@ function create(opts) {
|
|
|
494
531
|
switch (verb) {
|
|
495
532
|
case "EHLO":
|
|
496
533
|
case "HELO":
|
|
497
|
-
_handleEhlo(state, socket, line, verb);
|
|
534
|
+
await _handleEhlo(state, socket, line, verb);
|
|
498
535
|
return;
|
|
499
536
|
case "STARTTLS":
|
|
500
537
|
_handleStartTls(state, socket);
|
|
501
538
|
return;
|
|
502
539
|
case "MAIL":
|
|
503
|
-
_handleMailFrom(state, socket, line);
|
|
540
|
+
await _handleMailFrom(state, socket, line);
|
|
504
541
|
return;
|
|
505
542
|
case "RCPT":
|
|
506
|
-
_handleRcptTo(state, socket, line);
|
|
543
|
+
await _handleRcptTo(state, socket, line);
|
|
507
544
|
return;
|
|
508
545
|
case "DATA":
|
|
509
546
|
_handleData(state, socket);
|
|
@@ -531,7 +568,7 @@ function create(opts) {
|
|
|
531
568
|
}
|
|
532
569
|
|
|
533
570
|
// ---- EHLO / HELO ------------------------------------------------------
|
|
534
|
-
function _handleEhlo(state, socket, line, verb) {
|
|
571
|
+
async function _handleEhlo(state, socket, line, verb) {
|
|
535
572
|
var helo = line.slice(verb.length).trim();
|
|
536
573
|
if (!helo) {
|
|
537
574
|
_writeReply(socket, REPLY_501_BAD_ARGS, "5.5.4 " + verb + " requires a domain argument");
|
|
@@ -552,6 +589,25 @@ function create(opts) {
|
|
|
552
589
|
return;
|
|
553
590
|
}
|
|
554
591
|
}
|
|
592
|
+
// Operator HELO-identity gate (b.mail.helo) — FCrDNS / HELO-shape /
|
|
593
|
+
// self-name spoofing checks. Composed when the operator wires
|
|
594
|
+
// `opts.helo`; skipped silently otherwise (no synthesized verdict).
|
|
595
|
+
// Hard-reject actions (reject-shape / match-self-refused /
|
|
596
|
+
// literal-mismatch) refuse the connection; "accept" and the
|
|
597
|
+
// advisory "soft-*" actions pass (the soft verdict rides the event).
|
|
598
|
+
if (opts.helo && typeof opts.helo.evaluate === "function") {
|
|
599
|
+
var heloGate = await opts.helo.evaluate(
|
|
600
|
+
{ claimedName: helo, ip: state.remoteAddress, tls: state.tls }, {});
|
|
601
|
+
state.heloVerdict = heloGate && heloGate.action;
|
|
602
|
+
if (heloGate && heloGate.action && heloGate.action !== "accept" &&
|
|
603
|
+
heloGate.action.indexOf("soft") !== 0) {
|
|
604
|
+
_emit("mail.server.mx.helo_gate_refused",
|
|
605
|
+
{ connectionId: state.id, helo: helo, action: heloGate.action }, "denied");
|
|
606
|
+
_writeReply(socket, REPLY_550_MAILBOX_UNAVAIL,
|
|
607
|
+
"5.7.1 " + verb + " identity refused (" + heloGate.action + ")");
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
555
611
|
state.helo = helo;
|
|
556
612
|
state.stage = "ehlo";
|
|
557
613
|
// Multi-line 250 capabilities advertisement per RFC 5321 §4.1.1.1.
|
|
@@ -572,7 +628,8 @@ function create(opts) {
|
|
|
572
628
|
_writeReply(socket, REPLY_250_OK, greeting + " greets " + helo);
|
|
573
629
|
}
|
|
574
630
|
_emit("mail.server.mx.helo",
|
|
575
|
-
{ connectionId: state.id, verb: verb, helo: helo, tls: state.tls
|
|
631
|
+
{ connectionId: state.id, verb: verb, helo: helo, tls: state.tls,
|
|
632
|
+
heloVerdict: state.heloVerdict || null });
|
|
576
633
|
}
|
|
577
634
|
|
|
578
635
|
// ---- STARTTLS ---------------------------------------------------------
|
|
@@ -604,13 +661,11 @@ function create(opts) {
|
|
|
604
661
|
state.helo = null;
|
|
605
662
|
},
|
|
606
663
|
onData: function (tlsSocket, chunk) {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
_closeConnection(tlsSocket);
|
|
613
|
-
}
|
|
664
|
+
// Route the upgraded socket through the SAME serialized pump as
|
|
665
|
+
// the plaintext path — post-STARTTLS is where the gates run in
|
|
666
|
+
// the default strict/balanced profiles, so it MUST be serialized
|
|
667
|
+
// and its async rejections MUST hit the 421 path.
|
|
668
|
+
_feedChunk(tlsSocket, chunk);
|
|
614
669
|
},
|
|
615
670
|
onError: function (err) {
|
|
616
671
|
_emit("mail.server.mx.tls_handshake_failed",
|
|
@@ -626,7 +681,7 @@ function create(opts) {
|
|
|
626
681
|
}
|
|
627
682
|
|
|
628
683
|
// ---- MAIL FROM --------------------------------------------------------
|
|
629
|
-
function _handleMailFrom(state, socket, line) {
|
|
684
|
+
async function _handleMailFrom(state, socket, line) {
|
|
630
685
|
if (!state.tls && _requiresStartTls()) {
|
|
631
686
|
_writeReply(socket, REPLY_530_AUTH_REQUIRED, "5.7.0 Must issue a STARTTLS command first");
|
|
632
687
|
return;
|
|
@@ -677,7 +732,7 @@ function create(opts) {
|
|
|
677
732
|
}
|
|
678
733
|
|
|
679
734
|
// ---- RCPT TO ----------------------------------------------------------
|
|
680
|
-
function _handleRcptTo(state, socket, line) {
|
|
735
|
+
async function _handleRcptTo(state, socket, line) {
|
|
681
736
|
if (state.stage !== "rcpt") {
|
|
682
737
|
_writeReply(socket, REPLY_503_BAD_SEQUENCE, "5.5.1 MAIL FROM first");
|
|
683
738
|
return;
|
|
@@ -740,9 +795,49 @@ function create(opts) {
|
|
|
740
795
|
return;
|
|
741
796
|
}
|
|
742
797
|
}
|
|
798
|
+
// RBL gate (b.mail.rbl) — DNS blocklist check on the connecting
|
|
799
|
+
// IP. The verdict is per-connection, so it's evaluated once and
|
|
800
|
+
// cached on state; a listed IP refuses with 554. Skipped silently
|
|
801
|
+
// when opts.rbl isn't wired.
|
|
802
|
+
if (opts.rbl && typeof opts.rbl.query === "function") {
|
|
803
|
+
if (state.rblVerdict === undefined) {
|
|
804
|
+
state.rblVerdict = await opts.rbl.query(state.remoteAddress);
|
|
805
|
+
}
|
|
806
|
+
if (state.rblVerdict && Array.isArray(state.rblVerdict.listed) &&
|
|
807
|
+
state.rblVerdict.listed.length > 0) {
|
|
808
|
+
_trackRefusedRcpt(state, rcpt, "rbl-listed");
|
|
809
|
+
_emit("mail.server.mx.rbl_refused",
|
|
810
|
+
{ connectionId: state.id, remoteAddress: state.remoteAddress,
|
|
811
|
+
zones: state.rblVerdict.listed.map(function (l) { return l.zone; }) }, "denied");
|
|
812
|
+
_writeReply(socket, REPLY_554_TRANSACTION_FAILED,
|
|
813
|
+
"5.7.1 Connecting IP is on a DNS blocklist");
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
// Greylist gate (b.mail.greylist) — defer first sight of an
|
|
818
|
+
// (ip, mailFrom, rcpt) tuple with a 450 tempfail; legitimate
|
|
819
|
+
// senders retry and pass. "defer" → 450; "accept" → continue.
|
|
820
|
+
// Skipped silently when opts.greylist isn't wired.
|
|
821
|
+
var greyVerdict = null;
|
|
822
|
+
if (opts.greylist && typeof opts.greylist.check === "function") {
|
|
823
|
+
greyVerdict = await opts.greylist.check(
|
|
824
|
+
{ ip: state.remoteAddress, mailFrom: state.mailFrom || "", rcptTo: rcpt });
|
|
825
|
+
if (greyVerdict && greyVerdict.action === "defer") {
|
|
826
|
+
_emit("mail.server.mx.greylist_deferred",
|
|
827
|
+
{ connectionId: state.id, remoteAddress: state.remoteAddress,
|
|
828
|
+
mailFrom: state.mailFrom, rcptTo: rcpt,
|
|
829
|
+
reason: greyVerdict.reason }, "denied");
|
|
830
|
+
_writeReply(socket, REPLY_450_MAILBOX_BUSY,
|
|
831
|
+
"4.7.1 Greylisted — please retry shortly");
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
743
835
|
state.rcpts.push(rcpt);
|
|
744
836
|
_emit("mail.server.mx.rcpt_to",
|
|
745
|
-
{ connectionId: state.id, rcptTo: rcpt, rcptCount: state.rcpts.length
|
|
837
|
+
{ connectionId: state.id, rcptTo: rcpt, rcptCount: state.rcpts.length,
|
|
838
|
+
rblListed: !!(state.rblVerdict && Array.isArray(state.rblVerdict.listed) &&
|
|
839
|
+
state.rblVerdict.listed.length > 0),
|
|
840
|
+
greylist: greyVerdict ? greyVerdict.action : null });
|
|
746
841
|
_writeReply(socket, REPLY_250_OK, "2.1.5 Recipient OK");
|
|
747
842
|
}
|
|
748
843
|
|
|
@@ -764,7 +859,7 @@ function create(opts) {
|
|
|
764
859
|
});
|
|
765
860
|
}
|
|
766
861
|
|
|
767
|
-
function _finalizeDataBody(state, socket, body) {
|
|
862
|
+
async function _finalizeDataBody(state, socket, body) {
|
|
768
863
|
// body is the raw bytes BEFORE dot-stuffing reversal. RFC 5321
|
|
769
864
|
// §4.5.2 — a single leading "." is doubled on the wire; undo.
|
|
770
865
|
var dedotted = safeSmtp.dotUnstuff(body);
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
*
|
|
41
41
|
* ## Wire-protocol defenses (inherited from MX listener pattern)
|
|
42
42
|
*
|
|
43
|
-
* - SMTP smuggling (CVE-2023-51764 / -51765 / -51766 /
|
|
43
|
+
* - SMTP smuggling (CVE-2023-51764 / -51765 / -51766 /
|
|
44
44
|
* RFC 5321 §2.3.8): every wire line through
|
|
45
45
|
* `b.guardSmtpCommand.validate`; DATA-body terminator scan
|
|
46
46
|
* through `b.safeSmtp.findDotTerminator` (strict-CRLF);
|
|
@@ -351,8 +351,8 @@ function create(opts) {
|
|
|
351
351
|
}
|
|
352
352
|
|
|
353
353
|
// Default-on guardDomain hardening for HELO / MAIL FROM / RCPT TO.
|
|
354
|
-
// Same posture as mail-server-mx — IDN homograph
|
|
355
|
-
// class), special-use-domain refusal (RFC 6761), label-length cap
|
|
354
|
+
// Same posture as mail-server-mx — IDN homograph / Punycode-spoof
|
|
355
|
+
// (mixed-script confusable class), special-use-domain refusal (RFC 6761), label-length cap
|
|
356
356
|
// (RFC 1035 §2.3.4), bare-IP-as-domain refusal (CVE-2021-22931
|
|
357
357
|
// class). Operators with a closed-network deployment pass
|
|
358
358
|
// `guardDomain: false` to skip; the default keeps protection on.
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
* of caller; only `b.legalHold.release` can flip the flag.
|
|
51
51
|
*
|
|
52
52
|
* Parses messages on append via `b.safeMime.parse` (bounded
|
|
53
|
-
* substrate, defends CVE-2024-39929 + CVE-
|
|
53
|
+
* substrate, defends CVE-2024-39929 + CVE-2026-26312). Validates
|
|
54
54
|
* `Message-Id` via `b.guardMessageId.validate`.
|
|
55
55
|
*
|
|
56
56
|
* @card
|
|
@@ -526,7 +526,7 @@ function _appendMessage(args) {
|
|
|
526
526
|
"appendMessage: folder '" + args.folderName + "' not found");
|
|
527
527
|
}
|
|
528
528
|
|
|
529
|
-
// Parse via safe-mime — bounded; defends CVE-2024-39929 + CVE-
|
|
529
|
+
// Parse via safe-mime — bounded; defends CVE-2024-39929 + CVE-2026-26312.
|
|
530
530
|
var tree = safeMime.parse(buf, args.safeMimeOpts);
|
|
531
531
|
|
|
532
532
|
// Extract canonical fields.
|
|
@@ -1594,10 +1594,10 @@ function create(opts) {
|
|
|
1594
1594
|
var auditOn = opts.audit !== false;
|
|
1595
1595
|
|
|
1596
1596
|
// Default-on guardDomain hardening for every outbound recipient + the
|
|
1597
|
-
// sender address. Refuses
|
|
1597
|
+
// sender address. Refuses IDN homograph / mixed-script-confusable spoofs in
|
|
1598
1598
|
// recipient or from domains, RFC 6761 special-use domain names
|
|
1599
1599
|
// (`.localhost`, `.test`, `.invalid`, `.example`) in production sends,
|
|
1600
|
-
// RFC 1035 §2.3.4 label-length violations, and CVE-2021-22931
|
|
1600
|
+
// RFC 1035 §2.3.4 label-length violations, and CVE-2021-22931 class
|
|
1601
1601
|
// bare-IP-as-domain (DNS-rebinding allowlist-bypass class). Operators
|
|
1602
1602
|
// sending to address literals (`<x@[1.2.3.4]>`) — rare; mostly mailing-
|
|
1603
1603
|
// list internals — pass `guardDomain: false` to opt out, or pass
|
|
@@ -462,12 +462,15 @@ function applyToContext(opts) {
|
|
|
462
462
|
// setKeyShares(["X25519MLKEM768", "X25519"]) → string[] (after)
|
|
463
463
|
// resetKeyShares() → restores default
|
|
464
464
|
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
//
|
|
465
|
+
// PQ/T-hybrid named-group ordering (RFC 9794 is the PQ/T-hybrid
|
|
466
|
+
// *terminology*; the TLS codepoints come from the IANA TLS Supported
|
|
467
|
+
// Groups registry + draft-kwiatkowski-tls-ecdhe-mlkem +
|
|
468
|
+
// draft-ietf-tls-hybrid-design). The preferred groups (the first the peer
|
|
469
|
+
// mutually supports wins) put the IANA-registered hybrid named groups
|
|
470
|
+
// ahead of the classical fallback:
|
|
468
471
|
//
|
|
469
|
-
// X25519MLKEM768 — codepoint 0x11EC
|
|
470
|
-
// SecP256r1MLKEM768 — codepoint 0x11EB
|
|
472
|
+
// X25519MLKEM768 — codepoint 0x11EC (IANA; draft-kwiatkowski-tls-ecdhe-mlkem)
|
|
473
|
+
// SecP256r1MLKEM768 — codepoint 0x11EB (IANA; NIST-curve hybrid)
|
|
471
474
|
// (NIST-curve fallback for FIPS-mandated peers
|
|
472
475
|
// that refuse X25519)
|
|
473
476
|
// SecP384r1MLKEM1024 — draft-kwiatkowski-tls-ecdhe-mlkem-02 codepoint
|
|
@@ -520,7 +523,7 @@ function resetKeyShares() {
|
|
|
520
523
|
return getKeyShares();
|
|
521
524
|
}
|
|
522
525
|
|
|
523
|
-
// preferredGroups —
|
|
526
|
+
// preferredGroups — alias surface for the named-group list.
|
|
524
527
|
// `set(list)` overrides the default ordering; `get()` reads the active
|
|
525
528
|
// list; `reset()` restores the framework default. The setKeyShares /
|
|
526
529
|
// getKeyShares / resetKeyShares names are kept as the lower-level
|
|
@@ -604,7 +607,7 @@ function buildOptions(opts) {
|
|
|
604
607
|
|
|
605
608
|
// PQC group preference. Caller may narrow (drop a group) but not
|
|
606
609
|
// widen — every requested group must appear in the framework
|
|
607
|
-
// preferred list. Both `groups` (
|
|
610
|
+
// preferred list. Both `groups` (alias) and `ecdhCurve`
|
|
608
611
|
// (Node TLS option) are accepted; `groups` wins when both supplied.
|
|
609
612
|
var requested = null;
|
|
610
613
|
if (Array.isArray(opts.groups)) {
|
|
@@ -54,10 +54,12 @@
|
|
|
54
54
|
* bytes ever cross the audit boundary on the bomb-class path.
|
|
55
55
|
*
|
|
56
56
|
* Threat model:
|
|
57
|
-
* - **
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
57
|
+
* - **Decompression bomb** (CWE-409 — improper handling of highly
|
|
58
|
+
* compressed data; the classic 42.zip nested-bomb expands to
|
|
59
|
+
* petabytes from kilobytes) across gzip / deflate / brotli —
|
|
60
|
+
* the bounded-output cap + expansion-ratio cap refuse before the
|
|
61
|
+
* allocation, so no decompressed bytes are ever materialized past
|
|
62
|
+
* the cap.
|
|
61
63
|
* - **Efail-class** (CVE-2017-17688 / 17689) — operators decrypting
|
|
62
64
|
* MIME parts compose `b.safeDecompress` on the inner deflate
|
|
63
65
|
* streams; the bounded-output posture defeats the unbounded-
|
|
@@ -73,8 +75,8 @@
|
|
|
73
75
|
* - [RFC 1951](https://www.rfc-editor.org/rfc/rfc1951) deflate
|
|
74
76
|
* - [RFC 1952](https://www.rfc-editor.org/rfc/rfc1952) gzip
|
|
75
77
|
* - [RFC 7932](https://www.rfc-editor.org/rfc/rfc7932) brotli
|
|
76
|
-
* - [
|
|
77
|
-
*
|
|
78
|
+
* - [CWE-409](https://cwe.mitre.org/data/definitions/409.html) improper
|
|
79
|
+
* handling of highly compressed data (decompression bomb)
|
|
78
80
|
*/
|
|
79
81
|
|
|
80
82
|
var zlib = require("node:zlib");
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* delivery-time iTIP processing, and the scheduling primitives that
|
|
16
16
|
* compose against ical bytes.
|
|
17
17
|
*
|
|
18
|
-
* Defends
|
|
18
|
+
* Defends the ical4j RRULE-recursion expansion-DoS class ("Outlook
|
|
19
19
|
* calendar bomb" — a hostile RRULE with unbounded COUNT and
|
|
20
20
|
* recursive BYxxx expansion can pin a CalDAV server's CPU at 100%
|
|
21
21
|
* until the request times out). Caps:
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
* instances than this cap.
|
|
37
37
|
* - RRULE BYDAY / BYMONTH / BYMONTHDAY / BYHOUR / BYMINUTE /
|
|
38
38
|
* BYSECOND / BYSETPOS / BYWEEKNO / BYYEARDAY list-length cap
|
|
39
|
-
* (24 entries) — refused regardless of profile.
|
|
40
|
-
* achieves expansion blow-up by stacking long BYxxx lists.
|
|
39
|
+
* (24 entries) — refused regardless of profile. The recursion
|
|
40
|
+
* DoS achieves expansion blow-up by stacking long BYxxx lists.
|
|
41
41
|
*
|
|
42
42
|
* Header-injection / control-char defense: refuses NUL, C0 control
|
|
43
43
|
* bytes (other than TAB inside QUOTED-PRINTABLE-shaped values), and
|
|
@@ -75,8 +75,8 @@
|
|
|
75
75
|
* @card
|
|
76
76
|
* Bounded RFC 5545 iCalendar parser — caps total bytes, nesting
|
|
77
77
|
* depth, RRULE COUNT and BYxxx list-lengths; refuses NUL / C0 / DEL
|
|
78
|
-
* inside property values; allowlists property names; defends
|
|
79
|
-
*
|
|
78
|
+
* inside property values; allowlists property names; defends the
|
|
79
|
+
* ical4j RRULE-recursion expansion-DoS class (Outlook calendar-bomb).
|
|
80
80
|
*/
|
|
81
81
|
|
|
82
82
|
var C = require("./constants");
|
|
@@ -84,8 +84,8 @@ var { defineClass } = require("./framework-error");
|
|
|
84
84
|
|
|
85
85
|
var SafeIcalError = defineClass("SafeIcalError", { alwaysPermanent: true });
|
|
86
86
|
|
|
87
|
-
// RRULE caps are enforced regardless of profile —
|
|
88
|
-
// no safe permissive posture.
|
|
87
|
+
// RRULE caps are enforced regardless of profile — the recursion-DoS
|
|
88
|
+
// class has no safe permissive posture.
|
|
89
89
|
var RRULE_MAX_COUNT = 10000; // allow:raw-byte-literal — RFC 5545 §3.3.10 recurrence-count cap
|
|
90
90
|
var RRULE_MAX_BY_ENTRIES = 24; // allow:raw-byte-literal — BYxxx list-length cap
|
|
91
91
|
|
|
@@ -223,7 +223,7 @@ function parse(text, opts) {
|
|
|
223
223
|
if (byteLen > caps.maxBytes) {
|
|
224
224
|
throw new SafeIcalError("safe-ical/oversize-bytes",
|
|
225
225
|
"safeIcal.parse: input " + byteLen + " bytes exceeds maxBytes=" + caps.maxBytes +
|
|
226
|
-
" (
|
|
226
|
+
" (calendar-bomb defense)");
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
var lines = _unfold(s, caps);
|
|
@@ -466,7 +466,7 @@ function _parseComponent(lines, startIdx, ctx, depth) {
|
|
|
466
466
|
if (depth > ctx.caps.maxNestingDepth) {
|
|
467
467
|
throw new SafeIcalError("safe-ical/oversize-nesting",
|
|
468
468
|
"safeIcal.parse: nesting depth exceeds maxNestingDepth=" +
|
|
469
|
-
ctx.caps.maxNestingDepth + " (
|
|
469
|
+
ctx.caps.maxNestingDepth + " (calendar-bomb defense)");
|
|
470
470
|
}
|
|
471
471
|
ctx.componentCount += 1;
|
|
472
472
|
if (ctx.componentCount > ctx.caps.maxComponents) {
|
|
@@ -516,7 +516,7 @@ function _parseComponent(lines, startIdx, ctx, depth) {
|
|
|
516
516
|
"safeIcal.parse: unknown property '" + pn +
|
|
517
517
|
"' (extend via opts.extraProperties or use X- prefix)");
|
|
518
518
|
}
|
|
519
|
-
// RRULE caps —
|
|
519
|
+
// RRULE caps — recursion-DoS / calendar-bomb defense.
|
|
520
520
|
if (pn === "RRULE" || pn === "EXRULE") {
|
|
521
521
|
_validateRrule(ln.value);
|
|
522
522
|
}
|
|
@@ -558,7 +558,7 @@ function _validateRrule(value) {
|
|
|
558
558
|
if (!isFinite(n) || n < 0 || n > RRULE_MAX_COUNT) {
|
|
559
559
|
throw new SafeIcalError("safe-ical/oversize-rrule-count",
|
|
560
560
|
"safeIcal.parse: RRULE COUNT=" + val + " exceeds cap=" +
|
|
561
|
-
RRULE_MAX_COUNT + " (
|
|
561
|
+
RRULE_MAX_COUNT + " (calendar-bomb defense)");
|
|
562
562
|
}
|
|
563
563
|
} else if (key === "BYDAY" || key === "BYMONTH" || key === "BYMONTHDAY" ||
|
|
564
564
|
key === "BYHOUR" || key === "BYMINUTE" || key === "BYSECOND" ||
|
|
@@ -567,7 +567,7 @@ function _validateRrule(value) {
|
|
|
567
567
|
if (entries.length > RRULE_MAX_BY_ENTRIES) {
|
|
568
568
|
throw new SafeIcalError("safe-ical/oversize-rrule-by",
|
|
569
569
|
"safeIcal.parse: RRULE " + key + " list length " + entries.length +
|
|
570
|
-
" exceeds cap=" + RRULE_MAX_BY_ENTRIES + " (
|
|
570
|
+
" exceeds cap=" + RRULE_MAX_BY_ENTRIES + " (calendar-bomb defense)");
|
|
571
571
|
}
|
|
572
572
|
}
|
|
573
573
|
}
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* crypto.
|
|
28
28
|
*
|
|
29
29
|
* Defends `CVE-2024-39929` (Exim MIME multipart parser) and
|
|
30
|
-
* `CVE-
|
|
30
|
+
* `CVE-2026-26312` (Stalwart nested `message/rfc822` MIME OOM) by capping
|
|
31
31
|
* total parts, nesting depth, boundary length, header bytes,
|
|
32
32
|
* header-line bytes, decoded body bytes, message bytes — plus
|
|
33
33
|
* charset + transfer-encoding allowlists.
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
* incoming message above a threshold.
|
|
42
42
|
*
|
|
43
43
|
* @card
|
|
44
|
-
* Bounded MIME parser — walks RFC 5322 + 2045 / 2046 / 2047 + EAI message structure into a part tree with hard caps on depth, part count, body size, header bytes, and charset / transfer-encoding allowlists. Defends CVE-2024-39929 + CVE-
|
|
44
|
+
* Bounded MIME parser — walks RFC 5322 + 2045 / 2046 / 2047 + EAI message structure into a part tree with hard caps on depth, part count, body size, header bytes, and charset / transfer-encoding allowlists. Defends CVE-2024-39929 + CVE-2026-26312.
|
|
45
45
|
*/
|
|
46
46
|
|
|
47
47
|
var C = require("./constants");
|
|
@@ -332,13 +332,13 @@ function _parsePart(buf, ctx, depth) {
|
|
|
332
332
|
if (depth > ctx.maxNestingDepth) {
|
|
333
333
|
throw new SafeMimeError("safe-mime/oversize-nesting",
|
|
334
334
|
"safeMime.parse: nesting depth exceeded maxNestingDepth=" + ctx.maxNestingDepth +
|
|
335
|
-
" (CVE-2024-39929
|
|
335
|
+
" (CVE-2024-39929 class defense)");
|
|
336
336
|
}
|
|
337
337
|
ctx.partCount += 1;
|
|
338
338
|
if (ctx.partCount > ctx.maxParts) {
|
|
339
339
|
throw new SafeMimeError("safe-mime/oversize-part-count",
|
|
340
340
|
"safeMime.parse: total parts exceeded maxParts=" + ctx.maxParts +
|
|
341
|
-
" (CVE-2024-39929
|
|
341
|
+
" (CVE-2024-39929 class defense)");
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
var sep = _findHeaderBodySep(buf);
|
|
@@ -668,7 +668,7 @@ function _decodeRfc2047Words(value) {
|
|
|
668
668
|
raw = Buffer.from(text.replace(/_/g, " ").replace(/=([0-9A-Fa-f]{2})/g,
|
|
669
669
|
function (__, hex) { return String.fromCharCode(parseInt(hex, 16)); }), "binary"); // allow:raw-byte-literal — parseInt radix 16, not bytes
|
|
670
670
|
}
|
|
671
|
-
// RFC 2047 §5
|
|
671
|
+
// RFC 2047 §5 encoded-word header-injection defense — after
|
|
672
672
|
// base64 / Q-encoded decode, check the DECODED bytes for header
|
|
673
673
|
// separators (CR, LF, NUL). A sender that base64-encodes
|
|
674
674
|
// `\r\nBcc: attacker@x.com` would otherwise reach the consumer's
|
|
@@ -680,7 +680,7 @@ function _decodeRfc2047Words(value) {
|
|
|
680
680
|
if (b === 0x0d /* CR */ || b === 0x0a /* LF */ || b === 0x00 /* NUL */) {
|
|
681
681
|
throw new SafeMimeError("safe-mime/rfc2047-header-injection",
|
|
682
682
|
"RFC 2047 encoded-word decoded to bytes containing CR/LF/NUL " +
|
|
683
|
-
"(byte index " + bi + "); refusing per RFC 2047 §5
|
|
683
|
+
"(byte index " + bi + "); refusing per RFC 2047 §5 (encoded-word header injection)");
|
|
684
684
|
}
|
|
685
685
|
}
|
|
686
686
|
return _decodeBufferAs(raw, charset);
|
|
@@ -619,7 +619,7 @@ function parse(script, opts) {
|
|
|
619
619
|
* Parse-only validation — returns `{ ok, requiredCaps, issues }`
|
|
620
620
|
* shape mirroring the rest of the guard family. Operator-facing
|
|
621
621
|
* primitives that want a JMAP-style `SieveScript/validate` response
|
|
622
|
-
* (RFC
|
|
622
|
+
* (RFC 9661 — JMAP for Sieve Scripts) compose this and surface `issues` directly.
|
|
623
623
|
*
|
|
624
624
|
* @opts
|
|
625
625
|
* profile: "strict" | "balanced" | "permissive",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* - RFC 5321 §2.3.8 — line termination MUST be CRLF
|
|
24
24
|
* - RFC 5321 §4.5.2 — dot-stuffing on the SMTP body
|
|
25
25
|
* - RFC 5321 §4.1.1.4 — DATA command terminates with `<CRLF>.<CRLF>`
|
|
26
|
-
* - CVE-2023-51764 / -51765 / -51766
|
|
26
|
+
* - CVE-2023-51764 / -51765 / -51766 — SMTP
|
|
27
27
|
* smuggling (parsers that accept bare-LF dot-terminators).
|
|
28
28
|
* The guard primitive `b.guardSmtpCommand.detectBodySmuggling`
|
|
29
29
|
* owns smuggling detection; the safe-* terminator scanner
|