@blamejs/core 0.9.46 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +951 -893
- package/index.js +30 -0
- package/lib/_test/crypto-fixtures.js +67 -0
- package/lib/agent-event-bus.js +52 -6
- package/lib/agent-idempotency.js +169 -16
- package/lib/agent-orchestrator.js +263 -9
- package/lib/agent-posture-chain.js +163 -5
- package/lib/agent-saga.js +146 -16
- package/lib/agent-snapshot.js +349 -19
- package/lib/agent-stream.js +34 -2
- package/lib/agent-tenant.js +179 -23
- package/lib/agent-trace.js +84 -21
- package/lib/auth/aal.js +8 -1
- package/lib/auth/ciba.js +6 -1
- package/lib/auth/dpop.js +7 -2
- package/lib/auth/fal.js +17 -8
- package/lib/auth/jwt-external.js +128 -4
- package/lib/auth/oauth.js +232 -10
- package/lib/auth/oid4vci.js +67 -7
- package/lib/auth/openid-federation.js +71 -25
- package/lib/auth/passkey.js +140 -6
- package/lib/auth/sd-jwt-vc.js +67 -5
- package/lib/circuit-breaker.js +10 -2
- package/lib/compliance.js +176 -8
- package/lib/crypto-field.js +114 -14
- package/lib/crypto.js +216 -20
- package/lib/db.js +1 -0
- package/lib/guard-imap-command.js +335 -0
- package/lib/guard-jmap.js +321 -0
- package/lib/guard-managesieve-command.js +566 -0
- package/lib/guard-pop3-command.js +317 -0
- package/lib/guard-smtp-command.js +58 -3
- package/lib/mail-agent.js +20 -7
- package/lib/mail-arc-sign.js +12 -8
- package/lib/mail-auth.js +323 -34
- package/lib/mail-crypto-pgp.js +934 -0
- package/lib/mail-crypto-smime.js +340 -0
- package/lib/mail-crypto.js +108 -0
- package/lib/mail-dav.js +1224 -0
- package/lib/mail-deploy.js +492 -0
- package/lib/mail-dkim.js +431 -26
- package/lib/mail-journal.js +435 -0
- package/lib/mail-scan.js +502 -0
- package/lib/mail-server-imap.js +1102 -0
- package/lib/mail-server-jmap.js +488 -0
- package/lib/mail-server-managesieve.js +853 -0
- package/lib/mail-server-mx.js +164 -34
- package/lib/mail-server-pop3.js +836 -0
- package/lib/mail-server-rate-limit.js +269 -0
- package/lib/mail-server-submission.js +1032 -0
- package/lib/mail-server-tls.js +445 -0
- package/lib/mail-sieve.js +557 -0
- package/lib/mail-spam-score.js +284 -0
- package/lib/mail.js +99 -0
- package/lib/metrics.js +130 -10
- package/lib/middleware/dpop.js +58 -3
- package/lib/middleware/idempotency-key.js +255 -42
- package/lib/middleware/protected-resource-metadata.js +114 -2
- package/lib/network-dns-resolver.js +33 -0
- package/lib/network-tls.js +46 -0
- package/lib/outbox.js +62 -12
- package/lib/pqc-agent.js +13 -5
- package/lib/retry.js +23 -9
- package/lib/router.js +23 -1
- package/lib/safe-ical.js +634 -0
- package/lib/safe-icap.js +502 -0
- package/lib/safe-mime.js +15 -0
- package/lib/safe-sieve.js +684 -0
- package/lib/safe-smtp.js +57 -0
- package/lib/safe-url.js +37 -0
- package/lib/safe-vcard.js +473 -0
- package/lib/self-update-standalone-verifier.js +32 -3
- package/lib/self-update.js +168 -17
- package/lib/vendor/MANIFEST.json +161 -156
- package/lib/vendor-data.js +127 -9
- package/lib/vex.js +324 -59
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/mail-server-mx.js
CHANGED
|
@@ -116,7 +116,6 @@
|
|
|
116
116
|
*/
|
|
117
117
|
|
|
118
118
|
var net = require("node:net");
|
|
119
|
-
var nodeTls = require("node:tls");
|
|
120
119
|
var lazyRequire = require("./lazy-require");
|
|
121
120
|
var C = require("./constants");
|
|
122
121
|
var bCrypto = require("./crypto");
|
|
@@ -126,6 +125,9 @@ var safeBuffer = require("./safe-buffer");
|
|
|
126
125
|
var safeSmtp = require("./safe-smtp");
|
|
127
126
|
var validateOpts = require("./validate-opts");
|
|
128
127
|
var guardSmtpCommand = require("./guard-smtp-command");
|
|
128
|
+
var guardDomain = require("./guard-domain");
|
|
129
|
+
var mailServerRateLimit = require("./mail-server-rate-limit");
|
|
130
|
+
var mailServerTls = require("./mail-server-tls");
|
|
129
131
|
var { defineClass } = require("./framework-error");
|
|
130
132
|
|
|
131
133
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
@@ -208,7 +210,10 @@ function create(opts) {
|
|
|
208
210
|
MailServerMxError, "mail-server-mx/bad-opts");
|
|
209
211
|
if (!opts.tlsContext) {
|
|
210
212
|
throw new MailServerMxError("mail-server-mx/no-tls-context",
|
|
211
|
-
"mail.server.mx.create: tlsContext is required (no implicit plaintext mode)"
|
|
213
|
+
"mail.server.mx.create: tlsContext is required (no implicit plaintext mode). " +
|
|
214
|
+
"Use b.mail.server.tls.context({ certFile, keyFile, watch: true }) to load + " +
|
|
215
|
+
"auto-reload a cert/key pair from disk, or pass a node:tls.createSecureContext " +
|
|
216
|
+
"output directly. Cert provisioning lives in b.acme (RFC 8555 + RFC 9773 ARI).");
|
|
212
217
|
}
|
|
213
218
|
numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
|
|
214
219
|
["maxLineBytes", "maxMessageBytes", "maxRcptsPerMessage", "idleTimeoutMs"],
|
|
@@ -232,6 +237,66 @@ function create(opts) {
|
|
|
232
237
|
var relayAllowedFor = opts.relayAllowedFor || [];
|
|
233
238
|
var profile = opts.profile || "strict";
|
|
234
239
|
|
|
240
|
+
// Default-on per-IP rate limit. Operators pass `rateLimit: false` to
|
|
241
|
+
// disable (only for tests / closed networks), pass a rate-limit
|
|
242
|
+
// handle from b.mail.server.rateLimit.create({...}) to share one
|
|
243
|
+
// budget across multiple listeners, or pass an opts object to
|
|
244
|
+
// override defaults.
|
|
245
|
+
var rateLimit;
|
|
246
|
+
if (opts.rateLimit === false) {
|
|
247
|
+
rateLimit = mailServerRateLimit.create({ disabled: true });
|
|
248
|
+
} else if (opts.rateLimit && typeof opts.rateLimit.admitConnection === "function") {
|
|
249
|
+
rateLimit = opts.rateLimit;
|
|
250
|
+
} else {
|
|
251
|
+
rateLimit = mailServerRateLimit.create(opts.rateLimit || {});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Default-on operator-supplied-domain hardening. opts.localDomains
|
|
255
|
+
// and the HELO / MAIL FROM / RCPT TO domain validations all route
|
|
256
|
+
// through `b.guardDomain` for IDN homograph defense (CVE-2017-5469
|
|
257
|
+
// class), special-use-domain refusal (RFC 6761), label-length cap
|
|
258
|
+
// (RFC 1035 §2.3.4), and bare-IP-as-domain refusal (CVE-2021-22931
|
|
259
|
+
// class). Operators with a closed-network deployment can pass
|
|
260
|
+
// `guardDomain: false` to skip; the default keeps the protection on.
|
|
261
|
+
var guardDomainProfile;
|
|
262
|
+
if (opts.guardDomain === false) {
|
|
263
|
+
guardDomainProfile = null;
|
|
264
|
+
} else {
|
|
265
|
+
guardDomainProfile = guardDomain.buildProfile({
|
|
266
|
+
profile: opts.guardDomain && typeof opts.guardDomain === "object"
|
|
267
|
+
? (opts.guardDomain.profile || profile)
|
|
268
|
+
: profile,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
function _validateDomainHardened(d, label) {
|
|
272
|
+
if (!guardDomainProfile) return { ok: true };
|
|
273
|
+
var verdict = guardDomain.validate(d, guardDomainProfile);
|
|
274
|
+
if (!verdict.ok) {
|
|
275
|
+
_emit("mail.server.mx.domain_refused", {
|
|
276
|
+
reason: verdict.issues && verdict.issues[0] && verdict.issues[0].kind,
|
|
277
|
+
domain: d,
|
|
278
|
+
label: label,
|
|
279
|
+
}, "denied");
|
|
280
|
+
}
|
|
281
|
+
return verdict;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Pre-validate operator-supplied localDomains at boot — the same
|
|
285
|
+
// shape they enforce on RCPT TO must itself pass the validator,
|
|
286
|
+
// otherwise an operator who typed an IDN homograph (or an IP) into
|
|
287
|
+
// their allowlist would silently weaken the gate.
|
|
288
|
+
if (guardDomainProfile) {
|
|
289
|
+
for (var __ldi = 0; __ldi < localDomains.length; __ldi += 1) {
|
|
290
|
+
var __ldVerdict = guardDomain.validate(localDomains[__ldi], guardDomainProfile);
|
|
291
|
+
if (!__ldVerdict.ok) {
|
|
292
|
+
throw new MailServerMxError("mail-server-mx/bad-local-domain",
|
|
293
|
+
"mail.server.mx.create: localDomains[" + __ldi + "] '" + localDomains[__ldi] +
|
|
294
|
+
"' rejected by b.guardDomain (" +
|
|
295
|
+
(__ldVerdict.issues && __ldVerdict.issues[0] && __ldVerdict.issues[0].kind) + ")");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
235
300
|
var tcpServer = null;
|
|
236
301
|
var listening = false;
|
|
237
302
|
var connections = new Set();
|
|
@@ -248,19 +313,35 @@ function create(opts) {
|
|
|
248
313
|
|
|
249
314
|
// ---- Per-connection state machine ---------------------------------------
|
|
250
315
|
function _handleConnection(socket) {
|
|
316
|
+
var remoteAddress = socket.remoteAddress || "0.0.0.0";
|
|
317
|
+
var admit = rateLimit.admitConnection(remoteAddress);
|
|
318
|
+
if (!admit.ok) {
|
|
319
|
+
// 421 4.7.0 — transient refusal; sender retries elsewhere or later.
|
|
320
|
+
// RFC 5321 §3.8 + §4.5.4.2 (transient negative completion).
|
|
321
|
+
_emit("mail.server.mx.rate_limit_refused",
|
|
322
|
+
{ remoteAddress: remoteAddress, reason: admit.reason }, "denied");
|
|
323
|
+
try {
|
|
324
|
+
socket.write("421 4.7.0 Too many connections from your IP\r\n");
|
|
325
|
+
} catch (_e) { /* socket may already be torn down */ }
|
|
326
|
+
try { socket.destroy(); } catch (_e2) { /* idempotent */ }
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
socket.once("close", function () { rateLimit.releaseConnection(remoteAddress); });
|
|
330
|
+
|
|
251
331
|
var connectionId = "mxconn-" + bCrypto.generateToken(8); // allow:raw-byte-literal — connection-id length
|
|
252
332
|
connections.add(socket);
|
|
253
333
|
|
|
254
334
|
var state = {
|
|
255
335
|
id: connectionId,
|
|
256
|
-
remoteAddress:
|
|
257
|
-
remotePort: socket.remotePort
|
|
336
|
+
remoteAddress: remoteAddress,
|
|
337
|
+
remotePort: socket.remotePort || null,
|
|
258
338
|
tls: false,
|
|
259
339
|
stage: "connect", // connect | ehlo | mail | rcpt | data-body | done
|
|
260
340
|
helo: null,
|
|
261
341
|
mailFrom: null,
|
|
262
342
|
rcpts: [],
|
|
263
343
|
messageBytes: 0,
|
|
344
|
+
lastDataByteTime: 0,
|
|
264
345
|
};
|
|
265
346
|
|
|
266
347
|
var lineBuffer = "";
|
|
@@ -431,6 +512,21 @@ function create(opts) {
|
|
|
431
512
|
_writeReply(socket, REPLY_501_BAD_ARGS, "5.5.4 " + verb + " requires a domain argument");
|
|
432
513
|
return;
|
|
433
514
|
}
|
|
515
|
+
// Domain hardening for HELO/EHLO greeting (RFC 5321 §4.1.1.1).
|
|
516
|
+
// Skip when the greeting is an address literal (`[1.2.3.4]` /
|
|
517
|
+
// `[IPv6:...]`) — those are RFC-5321-legitimate non-domain
|
|
518
|
+
// forms; the bracket syntax is already constrained by
|
|
519
|
+
// b.guardSmtpCommand. Bare-IP-as-domain (no brackets) IS
|
|
520
|
+
// refused — that's the CVE-2021-22931 class guardDomain catches.
|
|
521
|
+
if (helo[0] !== "[" && guardDomainProfile) {
|
|
522
|
+
var heloVerdict = _validateDomainHardened(helo, "helo");
|
|
523
|
+
if (!heloVerdict.ok) {
|
|
524
|
+
_writeReply(socket, REPLY_501_BAD_ARGS,
|
|
525
|
+
"5.5.4 " + verb + " domain refused (" +
|
|
526
|
+
(heloVerdict.issues && heloVerdict.issues[0] && heloVerdict.issues[0].kind) + ")");
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
434
530
|
state.helo = helo;
|
|
435
531
|
state.stage = "ehlo";
|
|
436
532
|
// Multi-line 250 capabilities advertisement per RFC 5321 §4.1.1.1.
|
|
@@ -461,39 +557,46 @@ function create(opts) {
|
|
|
461
557
|
return;
|
|
462
558
|
}
|
|
463
559
|
_writeReply(socket, REPLY_220_READY, "2.0.0 Ready to start TLS");
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
// collector
|
|
467
|
-
//
|
|
468
|
-
//
|
|
560
|
+
// CVE-2021-38371 (Exim) / CVE-2021-33515 (Dovecot) STARTTLS-
|
|
561
|
+
// injection defense: clear the pre-handshake command buffer +
|
|
562
|
+
// body collector AND strip the plain-socket "data" listener
|
|
563
|
+
// before wrapping in TLSSocket so bytes the peer pipelined
|
|
564
|
+
// (RFC 2920) pre-handshake cannot reach the post-TLS state
|
|
565
|
+
// machine. Listener-removal + idle-timeout re-arm live in the
|
|
566
|
+
// shared upgradeSocket helper (b.mail.server.tls.upgradeSocket).
|
|
469
567
|
lineBuffer = "";
|
|
470
568
|
bodyCollector = null;
|
|
471
569
|
inDataBody = false;
|
|
472
|
-
|
|
473
|
-
|
|
570
|
+
mailServerTls.upgradeSocket({
|
|
571
|
+
plainSocket: socket,
|
|
474
572
|
secureContext: opts.tlsContext,
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
"
|
|
573
|
+
idleTimeoutMs: idleTimeoutMs,
|
|
574
|
+
onSecure: function (_tlsSocket) {
|
|
575
|
+
state.tls = true;
|
|
576
|
+
// After the handshake, the state machine restarts at EHLO
|
|
577
|
+
// (per RFC 3207 §4.2 — client MUST re-issue EHLO).
|
|
578
|
+
state.stage = "ehlo";
|
|
579
|
+
state.helo = null;
|
|
580
|
+
},
|
|
581
|
+
onData: function (tlsSocket, chunk) {
|
|
582
|
+
try { _ingestBytes(state, tlsSocket, chunk); }
|
|
583
|
+
catch (err) {
|
|
584
|
+
_emit("mail.server.mx.handler_threw",
|
|
585
|
+
{ connectionId: state.id, error: (err && err.message) || String(err) },
|
|
586
|
+
"failure");
|
|
587
|
+
_closeConnection(tlsSocket);
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
onError: function (err) {
|
|
591
|
+
_emit("mail.server.mx.tls_handshake_failed",
|
|
592
|
+
{ connectionId: state.id, code: (err && err.code) || "unknown",
|
|
593
|
+
message: err && err.message }, "failure");
|
|
594
|
+
_closeConnection(socket);
|
|
595
|
+
},
|
|
596
|
+
onTimeout: function (tlsSocket) {
|
|
597
|
+
_writeReply(tlsSocket, REPLY_421_SERVICE_NOT_AVAIL, "4.4.2 Idle timeout");
|
|
495
598
|
_closeConnection(tlsSocket);
|
|
496
|
-
}
|
|
599
|
+
},
|
|
497
600
|
});
|
|
498
601
|
}
|
|
499
602
|
|
|
@@ -514,6 +617,20 @@ function create(opts) {
|
|
|
514
617
|
return;
|
|
515
618
|
}
|
|
516
619
|
var mailFrom = match[1].toLowerCase();
|
|
620
|
+
// Domain hardening on MAIL FROM domain. Skip address-literal
|
|
621
|
+
// and empty-reverse-path forms (RFC 5321 §4.5.5 — bounce return
|
|
622
|
+
// path `<>` is legitimate and has no domain).
|
|
623
|
+
var __mfAtIdx = mailFrom.lastIndexOf("@");
|
|
624
|
+
var mailFromDomain = __mfAtIdx === -1 ? "" : mailFrom.slice(__mfAtIdx + 1);
|
|
625
|
+
if (mailFromDomain && mailFromDomain[0] !== "[" && guardDomainProfile) {
|
|
626
|
+
var mfVerdict = _validateDomainHardened(mailFromDomain, "mail_from");
|
|
627
|
+
if (!mfVerdict.ok) {
|
|
628
|
+
_writeReply(socket, REPLY_501_BAD_ARGS,
|
|
629
|
+
"5.5.4 MAIL FROM domain refused (" +
|
|
630
|
+
(mfVerdict.issues && mfVerdict.issues[0] && mfVerdict.issues[0].kind) + ")");
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
517
634
|
var paramStr = match[2] || "";
|
|
518
635
|
var sizeMatch = paramStr.match(RE_SIZE);
|
|
519
636
|
if (sizeMatch) {
|
|
@@ -549,11 +666,24 @@ function create(opts) {
|
|
|
549
666
|
return;
|
|
550
667
|
}
|
|
551
668
|
var rcpt = match[1].toLowerCase();
|
|
669
|
+
// Domain hardening on RCPT TO domain — skip the address-literal
|
|
670
|
+
// form per RFC 5321 §4.1.3 (bracket syntax already constrained
|
|
671
|
+
// by b.guardSmtpCommand). Refuses IDN homograph + special-use
|
|
672
|
+
// domains + bare-IP-as-domain on the un-bracketed form.
|
|
673
|
+
var _atIdx = rcpt.lastIndexOf("@");
|
|
674
|
+
var rcptDomain = _atIdx === -1 ? "" : rcpt.slice(_atIdx + 1);
|
|
675
|
+
if (rcptDomain && rcptDomain[0] !== "[" && guardDomainProfile) {
|
|
676
|
+
var rcptVerdict = _validateDomainHardened(rcptDomain, "rcpt_to");
|
|
677
|
+
if (!rcptVerdict.ok) {
|
|
678
|
+
_writeReply(socket, REPLY_501_BAD_ARGS,
|
|
679
|
+
"5.5.4 RCPT TO domain refused (" +
|
|
680
|
+
(rcptVerdict.issues && rcptVerdict.issues[0] && rcptVerdict.issues[0].kind) + ")");
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
552
684
|
// Local-domain check — refuse non-local recipients unless the
|
|
553
685
|
// operator explicitly allowed relay for this scope.
|
|
554
686
|
if (localDomains.length > 0) {
|
|
555
|
-
var atIdx = rcpt.lastIndexOf("@");
|
|
556
|
-
var rcptDomain = atIdx === -1 ? "" : rcpt.slice(atIdx + 1);
|
|
557
687
|
if (localDomains.indexOf(rcptDomain) === -1 &&
|
|
558
688
|
!_isRelayAllowed(state.remoteAddress, rcpt)) {
|
|
559
689
|
_emit("mail.server.mx.relay_refused",
|