@blamejs/core 0.18.53 → 0.18.55
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 +228 -0
- package/NOTICE +1 -1
- package/README.md +5 -5
- package/lib/agent-audit.js +27 -2
- package/lib/ai-adverse-decision.js +18 -2
- package/lib/audit-sign.js +24 -5
- package/lib/auth/passkey.js +4 -1
- package/lib/codepoint-class.js +72 -0
- package/lib/cookies.js +7 -10
- package/lib/credential-hash.js +8 -1
- package/lib/crypto.js +7 -5
- package/lib/db-file-lifecycle.js +14 -3
- package/lib/db.js +505 -49
- package/lib/guard-auth.js +34 -11
- package/lib/guard-filename.js +41 -33
- package/lib/guard-html.js +10 -2
- package/lib/guard-list-unsubscribe.js +6 -1
- package/lib/guard-managesieve-command.js +73 -12
- package/lib/guard-regex.js +3 -5
- package/lib/guard-smtp-command.js +20 -4
- package/lib/guard-svg.js +6 -1
- package/lib/guard-yaml.js +60 -15
- package/lib/http-client.js +17 -3
- package/lib/mail-agent.js +29 -13
- package/lib/mail-arc-sign.js +40 -7
- package/lib/mail-auth.js +134 -22
- package/lib/mail-crypto-pgp.js +1 -1
- package/lib/mail-dkim.js +80 -11
- package/lib/mail-helo.js +10 -0
- package/lib/mail-rbl.js +10 -3
- package/lib/mail-send-deliver.js +151 -32
- package/lib/mail-server-imap.js +186 -89
- package/lib/mail-server-jmap.js +31 -4
- package/lib/mail-server-managesieve.js +198 -42
- package/lib/mail-server-mx.js +191 -38
- package/lib/mail-server-net.js +281 -1
- package/lib/mail-server-pop3.js +89 -41
- package/lib/mail-server-rate-limit.js +104 -6
- package/lib/mail-server-submission.js +183 -35
- package/lib/mail-server-tls.js +48 -3
- package/lib/mail-store.js +33 -11
- package/lib/mail.js +355 -17
- package/lib/mcp.js +11 -3
- package/lib/middleware/bearer-auth.js +6 -1
- package/lib/middleware/fetch-metadata.js +5 -1
- package/lib/middleware/headers.js +7 -10
- package/lib/middleware/require-mtls.js +8 -1
- package/lib/network-dns-resolver.js +71 -8
- package/lib/network-dns.js +26 -0
- package/lib/network-smtp-policy.js +42 -10
- package/lib/network-tls.js +18 -0
- package/lib/redact.js +13 -3
- package/lib/retention.js +22 -2
- package/lib/safe-mount-info.js +39 -6
- package/lib/safe-smtp.js +96 -1
- package/lib/safe-url.js +8 -2
- package/lib/self-update.js +4 -1
- package/lib/vendor/MANIFEST.json +12 -12
- package/lib/vendor/blamejs-pki.cjs +672 -75
- package/lib/watcher.js +31 -6
- package/lib/ws-client.js +17 -2
- package/lib/yaml-lex.js +55 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -133,7 +133,6 @@ var safeBuffer = require("./safe-buffer");
|
|
|
133
133
|
var mailServerTls = require("./mail-server-tls");
|
|
134
134
|
var mailServerNet = require("./mail-server-net");
|
|
135
135
|
var C = require("./constants");
|
|
136
|
-
var bCrypto = require("./crypto");
|
|
137
136
|
var numericBounds = require("./numeric-bounds");
|
|
138
137
|
var validateOpts = require("./validate-opts");
|
|
139
138
|
var guardManageSieveCommand = require("./guard-managesieve-command");
|
|
@@ -172,6 +171,7 @@ var ERR_CLAMP = 200;
|
|
|
172
171
|
* greeting: string, // default "blamejs ManageSieve"
|
|
173
172
|
* maxLineBytes: number, // default 8192
|
|
174
173
|
* idleTimeoutMs: number, // default 5 min
|
|
174
|
+
* maxConnections: number, // default 1024 — listener-wide ceiling
|
|
175
175
|
* profile: "strict" | "balanced" | "permissive", // default "strict"
|
|
176
176
|
* auth: {
|
|
177
177
|
* mechanisms: ["SCRAM-SHA-256", "OAUTHBEARER", ...], // SASL mechs to advertise
|
|
@@ -212,7 +212,7 @@ function create(opts) {
|
|
|
212
212
|
"operator-supplied backend)");
|
|
213
213
|
}
|
|
214
214
|
numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
|
|
215
|
-
["maxLineBytes", "idleTimeoutMs"],
|
|
215
|
+
["maxLineBytes", "idleTimeoutMs", "maxConnections"],
|
|
216
216
|
"mail.server.managesieve.", MailServerManageSieveError, "mail-server-managesieve/bad-bound");
|
|
217
217
|
|
|
218
218
|
var greeting = opts.greeting || DEFAULT_GREETING_VENDOR;
|
|
@@ -236,22 +236,18 @@ function create(opts) {
|
|
|
236
236
|
var _emit = auditEmit.emit;
|
|
237
237
|
|
|
238
238
|
function _handleConnection(rawSocket) {
|
|
239
|
-
var
|
|
239
|
+
var accepted = mailServerNet.acceptConnection(rawSocket, {
|
|
240
|
+
rateLimit: rateLimit,
|
|
241
|
+
connections: connections,
|
|
242
|
+
emit: _emit,
|
|
240
243
|
refusedEvent: "mail.server.managesieve.rate_limit_refused",
|
|
241
244
|
refusalLine: 'NO "Too many connections from your IP"\r\n',
|
|
245
|
+
idPrefix: "msvconn-",
|
|
242
246
|
});
|
|
243
|
-
if (
|
|
244
|
-
var
|
|
245
|
-
var
|
|
246
|
-
|
|
247
|
-
// Single close handler covers BOTH operator-driven `_close(socket)`
|
|
248
|
-
// and client-initiated disconnects (TCP FIN / RST). Releases the
|
|
249
|
-
// rate-limit slot AND removes the socket from the tracking set so
|
|
250
|
-
// long-lived deployments don't accumulate stale entries.
|
|
251
|
-
rawSocket.once("close", function () {
|
|
252
|
-
rateLimit.releaseConnection(remoteAddress);
|
|
253
|
-
connections.delete(socket);
|
|
254
|
-
});
|
|
247
|
+
if (accepted === null) return;
|
|
248
|
+
var remoteAddress = accepted.remoteAddress;
|
|
249
|
+
var connectionId = accepted.connectionId;
|
|
250
|
+
var socket = accepted.socket;
|
|
255
251
|
|
|
256
252
|
var state = {
|
|
257
253
|
id: connectionId,
|
|
@@ -261,6 +257,8 @@ function create(opts) {
|
|
|
261
257
|
actor: null,
|
|
262
258
|
pendingLiteral: null, // { verb, name, size, body, plus }
|
|
263
259
|
pendingAuth: null, // { mech, irBytes, irPlus, irBody }
|
|
260
|
+
saslExchange: null, // { mech, step } while a multi-step SASL exchange is live
|
|
261
|
+
awaitingLiteralTerminator: false, // a literal body was read; its line's CRLF has not arrived
|
|
264
262
|
lineBuffer: Buffer.alloc(0),
|
|
265
263
|
};
|
|
266
264
|
|
|
@@ -300,6 +298,31 @@ function create(opts) {
|
|
|
300
298
|
// AUTHENTICATE with a non-synchronizing initial-response), the next
|
|
301
299
|
// N bytes are the literal-payload; accumulate them before resuming
|
|
302
300
|
// line-mode dispatch.
|
|
301
|
+
// The CRLF that ends the LINE a literal sat on. RFC 5804 §4 puts it after
|
|
302
|
+
// the payload, outside the declared octet count, so every literal owes one
|
|
303
|
+
// and no literal's size includes it.
|
|
304
|
+
//
|
|
305
|
+
// Consumed here when it is already buffered, and otherwise awaited: it may
|
|
306
|
+
// arrive in a later segment, and read as a line it is an EMPTY line. On the
|
|
307
|
+
// SASL path an empty line is a second response and the pipelining guard
|
|
308
|
+
// fails the exchange for it; on the PUTSCRIPT path it draws a spurious
|
|
309
|
+
// "empty command line" refusal AFTER the script was accepted, so one command
|
|
310
|
+
// is answered twice and a client that reads one reply per command spends the
|
|
311
|
+
// rest of the session attributing each answer to the command before it.
|
|
312
|
+
//
|
|
313
|
+
// A peek alone is not enough, because whether the terminator has arrived yet
|
|
314
|
+
// is a property of the network rather than of the protocol. Shared by both
|
|
315
|
+
// literal paths: one of them used to do this and the other did not, which is
|
|
316
|
+
// the entire defect.
|
|
317
|
+
function _consumeLiteralTerminator(state) {
|
|
318
|
+
if (state.lineBuffer.length >= 2 &&
|
|
319
|
+
state.lineBuffer[0] === 0x0d && state.lineBuffer[1] === 0x0a) { // CR LF
|
|
320
|
+
state.lineBuffer = state.lineBuffer.subarray(2);
|
|
321
|
+
} else {
|
|
322
|
+
state.awaitingLiteralTerminator = true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
303
326
|
function _drainBuffer(state, socket) {
|
|
304
327
|
while (true) {
|
|
305
328
|
if (state.pendingLiteral) {
|
|
@@ -312,6 +335,7 @@ function create(opts) {
|
|
|
312
335
|
}
|
|
313
336
|
pl.body = Buffer.concat([pl.body, state.lineBuffer.subarray(0, need)]);
|
|
314
337
|
state.lineBuffer = state.lineBuffer.subarray(need);
|
|
338
|
+
_consumeLiteralTerminator(state);
|
|
315
339
|
state.pendingLiteral = null;
|
|
316
340
|
_completePutscript(state, socket, pl);
|
|
317
341
|
if (state.stage === "closed") return;
|
|
@@ -327,7 +351,7 @@ function create(opts) {
|
|
|
327
351
|
}
|
|
328
352
|
pa.irBody = Buffer.concat([pa.irBody, state.lineBuffer.subarray(0, needA)]);
|
|
329
353
|
state.lineBuffer = state.lineBuffer.subarray(needA);
|
|
330
|
-
|
|
354
|
+
_consumeLiteralTerminator(state);
|
|
331
355
|
pa.irBytes = null;
|
|
332
356
|
_completeAuthenticate(state, socket);
|
|
333
357
|
if (state.stage === "closed") return;
|
|
@@ -343,12 +367,27 @@ function create(opts) {
|
|
|
343
367
|
}
|
|
344
368
|
var rawLine = state.lineBuffer.subarray(0, crlf).toString("utf8");
|
|
345
369
|
state.lineBuffer = state.lineBuffer.subarray(crlf + 2);
|
|
370
|
+
// The terminator a literal was still owed, arriving in its own segment.
|
|
371
|
+
// It closes the literal's line rather than opening a new one.
|
|
372
|
+
if (state.awaitingLiteralTerminator) {
|
|
373
|
+
state.awaitingLiteralTerminator = false;
|
|
374
|
+
if (rawLine.length === 0) continue;
|
|
375
|
+
}
|
|
346
376
|
_handleLine(state, socket, rawLine);
|
|
347
377
|
if (state.stage === "closed") return;
|
|
348
378
|
}
|
|
349
379
|
}
|
|
350
380
|
|
|
351
381
|
function _handleLine(state, socket, line) {
|
|
382
|
+
// Mid-SASL: this line is the client's base64 response to the server's
|
|
383
|
+
// challenge, not a ManageSieve verb, so it goes to the exchange rather than
|
|
384
|
+
// the wire guard — which would refuse it as an unknown command. This
|
|
385
|
+
// ordering is why the `overrides` hook could never carry a multi-step
|
|
386
|
+
// mechanism on its own: the guard ran first and the reply never arrived.
|
|
387
|
+
if (state.saslExchange) {
|
|
388
|
+
_continueSaslExchange(state, socket, line);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
352
391
|
var parsed;
|
|
353
392
|
try {
|
|
354
393
|
parsed = guardManageSieveCommand.validate(line, {
|
|
@@ -589,10 +628,19 @@ function create(opts) {
|
|
|
589
628
|
mech: mech,
|
|
590
629
|
irBytes: parsed.literalBytes,
|
|
591
630
|
irPlus: parsed.literalPlus,
|
|
592
|
-
|
|
631
|
+
// RFC 5804 §2.1 lets the initial response come inline as a quoted string
|
|
632
|
+
// instead of a literal. It arrives complete on this line, so there is
|
|
633
|
+
// nothing to wait for — the exchange runs now, with the response the
|
|
634
|
+
// client actually sent. This used to fall through to the no-initial-
|
|
635
|
+
// response branch and answer a conforming client as though it had said
|
|
636
|
+
// nothing.
|
|
637
|
+
irBody: parsed.initialResponse === null || parsed.initialResponse === undefined
|
|
638
|
+
? Buffer.alloc(0)
|
|
639
|
+
: Buffer.from(parsed.initialResponse, "utf8"),
|
|
593
640
|
};
|
|
594
641
|
if (parsed.literalBytes === null) {
|
|
595
|
-
//
|
|
642
|
+
// Either an inline initial response, complete above, or none at all —
|
|
643
|
+
// in which case verify is called with an empty client response.
|
|
596
644
|
_completeAuthenticate(state, socket);
|
|
597
645
|
return;
|
|
598
646
|
}
|
|
@@ -605,38 +653,145 @@ function create(opts) {
|
|
|
605
653
|
// after.
|
|
606
654
|
}
|
|
607
655
|
|
|
656
|
+
// `{N}` / `{N+}` on a line of its own, the RFC 5804 §1.2 literal form of a
|
|
657
|
+
// string. Returns null for anything else. Scanned rather than matched so the
|
|
658
|
+
// digit run is bounded by the line the caller already length-capped.
|
|
659
|
+
function _parseLiteralMarker(line) {
|
|
660
|
+
if (line.length < 3 || line.charAt(0) !== "{") return null;
|
|
661
|
+
var i = 1;
|
|
662
|
+
var digits = "";
|
|
663
|
+
while (i < line.length && line.charCodeAt(i) >= 0x30 && line.charCodeAt(i) <= 0x39) {
|
|
664
|
+
digits += line.charAt(i);
|
|
665
|
+
i += 1;
|
|
666
|
+
if (digits.length > 9) return null; // absurd count: not a literal we will honour
|
|
667
|
+
}
|
|
668
|
+
if (digits.length === 0) return null;
|
|
669
|
+
var plus = false;
|
|
670
|
+
if (line.charAt(i) === "+") { plus = true; i += 1; }
|
|
671
|
+
if (line.charAt(i) !== "}" || i !== line.length - 1) return null;
|
|
672
|
+
return { bytes: Number(digits), plus: plus };
|
|
673
|
+
}
|
|
674
|
+
|
|
608
675
|
function _completeAuthenticate(state, socket) {
|
|
609
676
|
var pa = state.pendingAuth;
|
|
610
677
|
state.pendingAuth = null;
|
|
611
678
|
if (!pa) return;
|
|
679
|
+
// Resuming a live exchange: the step counter and mechanism carry over, and
|
|
680
|
+
// no new auth_attempt is emitted — this is the same attempt, one round on.
|
|
681
|
+
if (pa.resume && state.saslExchange) {
|
|
682
|
+
_runAuthStep(state, socket, pa.irBody.toString("utf8"));
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
612
685
|
_emit("mail.server.managesieve.auth_attempt",
|
|
613
686
|
{ connectionId: state.id, mech: pa.mech, remoteAddress: state.remoteAddress });
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
687
|
+
state.saslExchange = { mech: pa.mech, step: 0 };
|
|
688
|
+
_runAuthStep(state, socket,
|
|
689
|
+
pa.irBody.length > 0 ? pa.irBody.toString("utf8") : null);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// One round of a SASL exchange (RFC 5804 §2.1). The verifier may answer with
|
|
693
|
+
// `{ pending: true, challenge }` to ask for another client response, as it
|
|
694
|
+
// may on the IMAP, POP3 and submission listeners. ManageSieve used to call
|
|
695
|
+
// verify once with no `step`, so a pending verdict landed in the failure
|
|
696
|
+
// branch — and spent the client's authentication-failure budget for what is a
|
|
697
|
+
// normal protocol round trip, weakening the very defence that budget exists
|
|
698
|
+
// to provide.
|
|
699
|
+
function _runAuthStep(state, socket, clientResponse) {
|
|
700
|
+
var ex = state.saslExchange;
|
|
701
|
+
function _fail(reason) {
|
|
702
|
+
state.saslExchange = null;
|
|
703
|
+
rateLimit.noteAuthFailure(state.remoteAddress);
|
|
704
|
+
_emit("mail.server.managesieve.auth_failed",
|
|
705
|
+
{ connectionId: state.id, mech: ex.mech, reason: reason }, "denied");
|
|
706
|
+
_writeNo(socket, "Authentication failed");
|
|
707
|
+
}
|
|
708
|
+
mailServerNet.runSaslStep({
|
|
709
|
+
exchange: ex,
|
|
710
|
+
verify: authConfig.verify,
|
|
711
|
+
credentials: { tls: state.tls, remoteAddress: state.remoteAddress },
|
|
712
|
+
clientResponse: clientResponse,
|
|
713
|
+
writeChallenge: function (ch) { return _writeChallenge(socket, ch); },
|
|
714
|
+
onChallengeUnsafe: function () { _fail("challenge-contains-line-terminator"); },
|
|
715
|
+
onSuccess: function (result) {
|
|
716
|
+
state.saslExchange = null;
|
|
717
|
+
state.actor = result.actor;
|
|
718
|
+
state.stage = "authenticated";
|
|
719
|
+
_emit("mail.server.managesieve.auth_success",
|
|
720
|
+
{ connectionId: state.id, mech: ex.mech, tenantId: state.actor.tenantId || null });
|
|
721
|
+
_writeOk(socket, "Authenticated");
|
|
722
|
+
},
|
|
723
|
+
onFailure: function (result) { _fail((result && result.reason) || "verify-returned-fail"); },
|
|
724
|
+
onError: function (err) { _fail((err && err.message) || String(err)); },
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// RFC 5804 §2.1 — the server's SASL challenge is sent as a literal:
|
|
729
|
+
// `{N}\r\n<challenge>\r\n`. Returns false when the challenge carries bytes
|
|
730
|
+
// that would end the line, so the caller fails the exchange rather than
|
|
731
|
+
// emitting a second, smuggled protocol line.
|
|
732
|
+
function _writeChallenge(socket, challenge) {
|
|
733
|
+
var safe = mailServerNet.saslChallengeOrNull(challenge);
|
|
734
|
+
if (safe === null) return false;
|
|
735
|
+
try { socket.write("{" + Buffer.byteLength(safe, "utf8") + "}\r\n" + safe + "\r\n"); }
|
|
736
|
+
catch (_e) { /* socket down */ }
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// The client's reply to a challenge, per RFC 5804 §1.2 "string": a quoted
|
|
741
|
+
// string, or (accepted defensively) a bare token. `"*"` cancels the exchange
|
|
742
|
+
// — a withdrawal rather than a failed credential, so it costs no budget.
|
|
743
|
+
function _continueSaslExchange(state, socket, line) {
|
|
744
|
+
// RFC 5804 §1.2 — a "string" is a quoted string OR a literal. A SASL
|
|
745
|
+
// response is base64 and can be long, which is exactly when a client
|
|
746
|
+
// reaches for the literal form, and reading `{N+}` as the response itself
|
|
747
|
+
// then reading its bytes as a second response cannot authenticate anyone.
|
|
748
|
+
// The bytes are collected by the same drain-loop branch the initial
|
|
749
|
+
// AUTHENTICATE response uses, and `resume` tells _completeAuthenticate to
|
|
750
|
+
// continue this exchange rather than start a new one at step 0.
|
|
751
|
+
var lit = _parseLiteralMarker(line);
|
|
752
|
+
if (lit) {
|
|
753
|
+
// The client declaring this size is unauthenticated, and the drain loop
|
|
754
|
+
// will collect toward it. Unbounded, `{999999999+}` both consumes memory
|
|
755
|
+
// and pins the connection open waiting for bytes that never arrive.
|
|
756
|
+
//
|
|
757
|
+
// The bound is the guard's own SASL-token cap, the same one it applies to
|
|
758
|
+
// the AUTHENTICATE initial response. One exchange, one bound, whichever
|
|
759
|
+
// round the token arrives on: two numbers for the same thing is how the
|
|
760
|
+
// two halves of a protocol drift apart.
|
|
761
|
+
if (lit.bytes > guardManageSieveCommand.MAX_SASL_TOKEN_BYTES) {
|
|
762
|
+
var oversizeMech = state.saslExchange.mech;
|
|
763
|
+
state.saslExchange = null;
|
|
631
764
|
rateLimit.noteAuthFailure(state.remoteAddress);
|
|
632
765
|
_emit("mail.server.managesieve.auth_failed",
|
|
633
|
-
{ connectionId: state.id, mech:
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
766
|
+
{ connectionId: state.id, mech: oversizeMech,
|
|
767
|
+
reason: "continuation-literal-too-large" }, "denied");
|
|
768
|
+
_writeNo(socket, "Authentication response too long (cap " +
|
|
769
|
+
guardManageSieveCommand.MAX_SASL_TOKEN_BYTES + ")");
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
state.pendingAuth = {
|
|
773
|
+
mech: state.saslExchange.mech,
|
|
774
|
+
irBytes: lit.bytes,
|
|
775
|
+
irPlus: lit.plus,
|
|
776
|
+
irBody: Buffer.alloc(0),
|
|
777
|
+
resume: true,
|
|
778
|
+
};
|
|
779
|
+
// Synchronizing form: the server invites the bytes before the client
|
|
780
|
+
// sends them, exactly as it does for an initial-response literal.
|
|
781
|
+
if (!lit.plus) socket.write("OK\r\n");
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
// The guard owns the quoted form of the same production — escapes
|
|
785
|
+
// included, and NUL / CR / LF refused inside the quotes. A bare token is
|
|
786
|
+
// accepted defensively for clients that omit the quoting.
|
|
787
|
+
var quoted = guardManageSieveCommand.parseQuotedString(line);
|
|
788
|
+
var body = quoted ? quoted.value : line;
|
|
789
|
+
if (body === "*") {
|
|
790
|
+
state.saslExchange = null;
|
|
791
|
+
_writeNo(socket, "Authentication cancelled");
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
_runAuthStep(state, socket, body);
|
|
640
795
|
}
|
|
641
796
|
|
|
642
797
|
function _requireAuth(state, socket) {
|
|
@@ -844,6 +999,7 @@ function create(opts) {
|
|
|
844
999
|
// ---- Lifecycle ----------------------------------------------------------
|
|
845
1000
|
return mailServerNet.createStoreServer(net, {
|
|
846
1001
|
defaultPort: DEFAULT_PORT,
|
|
1002
|
+
maxConnections: opts.maxConnections,
|
|
847
1003
|
handleConnection: _handleConnection,
|
|
848
1004
|
errorClass: MailServerManageSieveError,
|
|
849
1005
|
errorCodePrefix: "mail-server-managesieve/",
|