@blamejs/core 0.9.49 → 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 -908
- package/index.js +25 -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-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 +64 -26
- package/lib/mail-server-jmap.js +488 -0
- package/lib/mail-server-managesieve.js +853 -0
- package/lib/mail-server-mx.js +40 -30
- package/lib/mail-server-pop3.js +836 -0
- package/lib/mail-server-rate-limit.js +13 -0
- package/lib/mail-server-submission.js +70 -24
- 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 +80 -3
- 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 +153 -33
- 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
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.guardPop3Command
|
|
4
|
+
* @nav Guards
|
|
5
|
+
* @title Guard POP3 Command
|
|
6
|
+
* @order 453
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* POP3 command-line validator (RFC 1939 Post Office Protocol — Version 3).
|
|
10
|
+
* Gates every command verb the framework's POP3 listener accepts
|
|
11
|
+
* from peers — `USER` / `PASS` / `APOP` / `AUTH` / `STLS` / `CAPA` /
|
|
12
|
+
* `STAT` / `LIST` / `RETR` / `DELE` / `NOOP` / `RSET` / `TOP` / `UIDL`
|
|
13
|
+
* / `QUIT`.
|
|
14
|
+
*
|
|
15
|
+
* POP3 is a simple line-oriented text protocol. Each command is a
|
|
16
|
+
* single CRLF-terminated line; responses are `+OK ...` or `-ERR ...`
|
|
17
|
+
* single-line or multi-line (terminated by `.` on a line of its own).
|
|
18
|
+
*
|
|
19
|
+
* ## Smuggling defense — bare-CR / bare-LF refusal
|
|
20
|
+
*
|
|
21
|
+
* Same wire-protocol concern as SMTP / IMAP. POP3's `.<CRLF>`
|
|
22
|
+
* end-of-multiline terminator is matched on canonical CRLF only;
|
|
23
|
+
* bare-LF dot-terminators are refused. Command lines themselves
|
|
24
|
+
* must be CRLF-terminated and contain no bare CR or LF.
|
|
25
|
+
*
|
|
26
|
+
* ## STLS injection
|
|
27
|
+
*
|
|
28
|
+
* RFC 2595 STLS upgrade (POP3's equivalent of STARTTLS) is subject
|
|
29
|
+
* to the same pre-handshake command-buffer injection class as
|
|
30
|
+
* SMTP / IMAP STARTTLS (CVE-2021-38371 Exim, CVE-2021-33515
|
|
31
|
+
* Dovecot). This guard refuses trailing payload on the STLS line;
|
|
32
|
+
* the listener's STLS handler is responsible for draining the
|
|
33
|
+
* pre-handshake buffer.
|
|
34
|
+
*
|
|
35
|
+
* ## Per-verb shape
|
|
36
|
+
*
|
|
37
|
+
* RFC 1939 §6 and RFC 2449 §5 define the verbs:
|
|
38
|
+
*
|
|
39
|
+
* - `USER` <name> — single argument
|
|
40
|
+
* - `PASS` <password> — single argument; refuse in CAPA
|
|
41
|
+
* (operator must rely on TLS confidentiality)
|
|
42
|
+
* - `APOP` <name> <digest> — RFC 1939 §7 challenge-response (legacy)
|
|
43
|
+
* - `AUTH` [<sasl-mech>] — RFC 5034 SASL framework (PLAIN /
|
|
44
|
+
* CRAM-MD5 / SCRAM-SHA-256 / EXTERNAL)
|
|
45
|
+
* - `STLS` — RFC 2595 §4 TLS upgrade
|
|
46
|
+
* - `CAPA` — RFC 2449 §5 capability discovery
|
|
47
|
+
* - `STAT` — no args
|
|
48
|
+
* - `LIST` [msg] — optional msg-number argument
|
|
49
|
+
* - `RETR` <msg> — single message-number argument
|
|
50
|
+
* - `DELE` <msg> — single message-number argument
|
|
51
|
+
* - `NOOP` — no args
|
|
52
|
+
* - `RSET` — no args
|
|
53
|
+
* - `TOP` <msg> <n> — RFC 2449 §5 — message + header-line count
|
|
54
|
+
* - `UIDL` [msg] — RFC 1939 §7 — optional msg arg
|
|
55
|
+
* - `QUIT` — no args
|
|
56
|
+
*
|
|
57
|
+
* ## Caps
|
|
58
|
+
*
|
|
59
|
+
* - Command line capped at 255 bytes per RFC 2449 §4 (response
|
|
60
|
+
* lines are 512 octets including CRLF; the command-line cap is
|
|
61
|
+
* even tighter).
|
|
62
|
+
* - Username + password capped at 40 octets each per RFC 1939 §3
|
|
63
|
+
* (longer values accepted under permissive but the wire is
|
|
64
|
+
* interpretation-defined).
|
|
65
|
+
* - Message-number capped at 10-decimal-digit positive integer.
|
|
66
|
+
*
|
|
67
|
+
* Throws `GuardPop3CommandError` on every refusal.
|
|
68
|
+
*
|
|
69
|
+
* @card
|
|
70
|
+
* POP3 command-line validator (RFC 1939 + RFC 2449 capabilities +
|
|
71
|
+
* RFC 2595 STLS + RFC 5034 AUTH). Refuses bare-CR / bare-LF
|
|
72
|
+
* (smuggling defense), caps command-line / username / password / msg
|
|
73
|
+
* bytes, validates per-verb shape.
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
var { defineClass } = require("./framework-error");
|
|
77
|
+
|
|
78
|
+
var GuardPop3CommandError = defineClass("GuardPop3CommandError", { alwaysPermanent: true });
|
|
79
|
+
|
|
80
|
+
var DEFAULT_PROFILE = "strict";
|
|
81
|
+
|
|
82
|
+
var PROFILES = Object.freeze({
|
|
83
|
+
strict: {
|
|
84
|
+
maxLineBytes: 255, // allow:raw-byte-literal — RFC 2449 §4 cap
|
|
85
|
+
maxUsernameBytes: 40, // allow:raw-byte-literal — RFC 1939 §3 cap
|
|
86
|
+
maxPasswordBytes: 40, // allow:raw-byte-literal — RFC 1939 §3 cap
|
|
87
|
+
allowBareLf: false,
|
|
88
|
+
allowApop: false, // RFC 1939 §7 — legacy challenge-response with MD5; refuse under strict (M³AAWG)
|
|
89
|
+
},
|
|
90
|
+
balanced: {
|
|
91
|
+
maxLineBytes: 512, // allow:raw-byte-literal — RFC 2449 §4 response cap
|
|
92
|
+
maxUsernameBytes: 128, // allow:raw-byte-literal — balanced username cap
|
|
93
|
+
maxPasswordBytes: 128, // allow:raw-byte-literal — balanced password cap
|
|
94
|
+
allowBareLf: false,
|
|
95
|
+
allowApop: true,
|
|
96
|
+
},
|
|
97
|
+
permissive: {
|
|
98
|
+
maxLineBytes: 1024, // allow:raw-byte-literal — permissive cap for legacy peers
|
|
99
|
+
maxUsernameBytes: 256, // allow:raw-byte-literal — permissive username cap
|
|
100
|
+
maxPasswordBytes: 256, // allow:raw-byte-literal — permissive password cap
|
|
101
|
+
allowBareLf: true,
|
|
102
|
+
allowApop: true,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
var COMPLIANCE_POSTURES = Object.freeze({
|
|
107
|
+
hipaa: "strict",
|
|
108
|
+
"pci-dss": "strict",
|
|
109
|
+
gdpr: "strict",
|
|
110
|
+
soc2: "strict",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// POP3 verbs per RFC 1939 §6 + RFC 2449 §5 + RFC 2595 §4 + RFC 5034.
|
|
114
|
+
var KNOWN_VERBS = Object.freeze({
|
|
115
|
+
USER: true, PASS: true, APOP: true, AUTH: true, STLS: true,
|
|
116
|
+
CAPA: true, STAT: true, LIST: true, RETR: true, DELE: true,
|
|
117
|
+
NOOP: true, RSET: true, TOP: true, UIDL: true, QUIT: true,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
var ZERO_ARG_VERBS = Object.freeze({
|
|
121
|
+
STLS: true, CAPA: true, STAT: true, NOOP: true, RSET: true, QUIT: true,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
var MSG_NUM_RE = /^[1-9][0-9]{0,9}$/; // allow:regex-no-length-cap — anchored + bounded repeat
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @primitive b.guardPop3Command.validate
|
|
128
|
+
* @signature b.guardPop3Command.validate(line, opts?)
|
|
129
|
+
* @since 0.9.52
|
|
130
|
+
* @status stable
|
|
131
|
+
* @related b.guardSmtpCommand.validate, b.guardImapCommand.validate
|
|
132
|
+
*
|
|
133
|
+
* Validate a single POP3 command line (without its CRLF terminator).
|
|
134
|
+
* Returns `{ verb, args }` on success; throws `GuardPop3CommandError`
|
|
135
|
+
* on refusal.
|
|
136
|
+
*
|
|
137
|
+
* @opts
|
|
138
|
+
* profile: "strict" | "balanced" | "permissive",
|
|
139
|
+
* posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
|
|
140
|
+
* tls: boolean, // when false + verb is USER/PASS under
|
|
141
|
+
* strict, refuse with `guard-pop3-command/
|
|
142
|
+
* cleartext-auth` (TLS required for credentials)
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* var parsed = b.guardPop3Command.validate("USER alice", { tls: true });
|
|
146
|
+
* // → { verb: "USER", args: ["alice"] }
|
|
147
|
+
*
|
|
148
|
+
* var pending = b.guardPop3Command.validate("RETR 12");
|
|
149
|
+
* // → { verb: "RETR", args: ["12"] }
|
|
150
|
+
*/
|
|
151
|
+
function validate(line, opts) {
|
|
152
|
+
opts = opts || {};
|
|
153
|
+
var profileName = typeof opts.profile === "string" ? opts.profile : DEFAULT_PROFILE;
|
|
154
|
+
if (opts.posture && COMPLIANCE_POSTURES[opts.posture]) {
|
|
155
|
+
profileName = COMPLIANCE_POSTURES[opts.posture];
|
|
156
|
+
}
|
|
157
|
+
var caps = PROFILES[profileName];
|
|
158
|
+
if (!caps) {
|
|
159
|
+
throw new GuardPop3CommandError("guard-pop3-command/bad-profile",
|
|
160
|
+
"guardPop3Command.validate: unknown profile '" + profileName + "'");
|
|
161
|
+
}
|
|
162
|
+
if (typeof line !== "string") {
|
|
163
|
+
throw new GuardPop3CommandError("guard-pop3-command/bad-input",
|
|
164
|
+
"guardPop3Command.validate: line must be a string");
|
|
165
|
+
}
|
|
166
|
+
if (line.length === 0) {
|
|
167
|
+
throw new GuardPop3CommandError("guard-pop3-command/empty-line",
|
|
168
|
+
"guardPop3Command.validate: empty command line");
|
|
169
|
+
}
|
|
170
|
+
if (line.length > caps.maxLineBytes) {
|
|
171
|
+
throw new GuardPop3CommandError("guard-pop3-command/line-too-long",
|
|
172
|
+
"guardPop3Command.validate: line " + line.length + " bytes exceeds cap " + caps.maxLineBytes);
|
|
173
|
+
}
|
|
174
|
+
for (var i = 0; i < line.length; i += 1) {
|
|
175
|
+
var c = line.charCodeAt(i);
|
|
176
|
+
if (c === 0x00 || c === 0x7F || (c < 0x20 && c !== 0x09)) { // allow:raw-byte-literal — control-byte refusal
|
|
177
|
+
if (c === 0x0A && caps.allowBareLf) continue;
|
|
178
|
+
throw new GuardPop3CommandError("guard-pop3-command/bad-byte",
|
|
179
|
+
"guardPop3Command.validate: control byte 0x" + c.toString(16) + " at offset " + i); // allow:raw-byte-literal — hex format literal in error message
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
var firstSpace = line.indexOf(" ");
|
|
184
|
+
var verb = (firstSpace === -1 ? line : line.slice(0, firstSpace)).toUpperCase();
|
|
185
|
+
var rest = firstSpace === -1 ? "" : line.slice(firstSpace + 1);
|
|
186
|
+
|
|
187
|
+
if (!KNOWN_VERBS[verb]) {
|
|
188
|
+
throw new GuardPop3CommandError("guard-pop3-command/unknown-verb",
|
|
189
|
+
"guardPop3Command.validate: unknown verb '" + verb + "' (RFC 1939 §6)");
|
|
190
|
+
}
|
|
191
|
+
if (ZERO_ARG_VERBS[verb] && rest.length > 0) {
|
|
192
|
+
throw new GuardPop3CommandError("guard-pop3-command/unexpected-args",
|
|
193
|
+
"guardPop3Command.validate: verb '" + verb + "' takes no arguments");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Per-verb shape — switch dispatch (statically resolved, not
|
|
197
|
+
// dynamic; CodeQL accepts switch as a fixed call graph).
|
|
198
|
+
var args = [];
|
|
199
|
+
switch (verb) {
|
|
200
|
+
case "USER":
|
|
201
|
+
if (!rest) throw new GuardPop3CommandError("guard-pop3-command/missing-username",
|
|
202
|
+
"guardPop3Command.validate: USER requires a name argument");
|
|
203
|
+
if (rest.length > caps.maxUsernameBytes) {
|
|
204
|
+
throw new GuardPop3CommandError("guard-pop3-command/username-too-long",
|
|
205
|
+
"guardPop3Command.validate: USER name " + rest.length + " bytes exceeds cap " + caps.maxUsernameBytes);
|
|
206
|
+
}
|
|
207
|
+
if (opts.tls === false && profileName === "strict") {
|
|
208
|
+
throw new GuardPop3CommandError("guard-pop3-command/cleartext-auth",
|
|
209
|
+
"guardPop3Command.validate: USER refused over cleartext (use STLS first; RFC 2595)");
|
|
210
|
+
}
|
|
211
|
+
args = [rest];
|
|
212
|
+
break;
|
|
213
|
+
case "PASS":
|
|
214
|
+
if (!rest) throw new GuardPop3CommandError("guard-pop3-command/missing-password",
|
|
215
|
+
"guardPop3Command.validate: PASS requires a password argument");
|
|
216
|
+
if (rest.length > caps.maxPasswordBytes) {
|
|
217
|
+
throw new GuardPop3CommandError("guard-pop3-command/password-too-long",
|
|
218
|
+
"guardPop3Command.validate: PASS argument " + rest.length + " bytes exceeds cap " + caps.maxPasswordBytes);
|
|
219
|
+
}
|
|
220
|
+
if (opts.tls === false && profileName === "strict") {
|
|
221
|
+
throw new GuardPop3CommandError("guard-pop3-command/cleartext-auth",
|
|
222
|
+
"guardPop3Command.validate: PASS refused over cleartext (use STLS first; RFC 2595)");
|
|
223
|
+
}
|
|
224
|
+
args = [rest];
|
|
225
|
+
break;
|
|
226
|
+
case "APOP":
|
|
227
|
+
if (!caps.allowApop) {
|
|
228
|
+
throw new GuardPop3CommandError("guard-pop3-command/apop-refused",
|
|
229
|
+
"guardPop3Command.validate: APOP refused under profile '" + profileName +
|
|
230
|
+
"' (RFC 1939 §7 uses MD5 challenge-response; deprecated by M³AAWG)");
|
|
231
|
+
}
|
|
232
|
+
var apopParts = rest.split(" ");
|
|
233
|
+
if (apopParts.length !== 2) {
|
|
234
|
+
throw new GuardPop3CommandError("guard-pop3-command/bad-apop",
|
|
235
|
+
"guardPop3Command.validate: APOP requires `name digest`");
|
|
236
|
+
}
|
|
237
|
+
args = apopParts;
|
|
238
|
+
break;
|
|
239
|
+
case "AUTH":
|
|
240
|
+
// RFC 5034 — `AUTH` alone lists supported mechanisms; `AUTH MECH`
|
|
241
|
+
// initiates a mechanism. Allow either shape.
|
|
242
|
+
args = rest ? rest.split(" ") : [];
|
|
243
|
+
// RFC 2595 §2.1 + RFC 5034 §4 — credentials over cleartext are
|
|
244
|
+
// refused under strict identically to USER/PASS. `AUTH` with no
|
|
245
|
+
// mech argument is a CAPA-style enumeration and stays allowed
|
|
246
|
+
// pre-TLS; a mech-bearing AUTH initiates the credential exchange
|
|
247
|
+
// and MUST be over TLS.
|
|
248
|
+
if (args.length > 0 && opts.tls === false && profileName === "strict") {
|
|
249
|
+
throw new GuardPop3CommandError("guard-pop3-command/cleartext-auth",
|
|
250
|
+
"guardPop3Command.validate: AUTH " + args[0] + " refused over cleartext (use STLS first; RFC 2595 §2.1 + RFC 5034 §4)");
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
case "LIST":
|
|
254
|
+
case "UIDL":
|
|
255
|
+
// Optional msg-number argument.
|
|
256
|
+
if (rest) {
|
|
257
|
+
if (!MSG_NUM_RE.test(rest)) { // allow:regex-no-length-cap — MSG_NUM_RE anchored + bounded
|
|
258
|
+
throw new GuardPop3CommandError("guard-pop3-command/bad-msg-number",
|
|
259
|
+
"guardPop3Command.validate: " + verb + " msg-number must be a positive decimal integer");
|
|
260
|
+
}
|
|
261
|
+
args = [rest];
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
case "RETR":
|
|
265
|
+
case "DELE":
|
|
266
|
+
if (!rest || !MSG_NUM_RE.test(rest)) { // allow:regex-no-length-cap — MSG_NUM_RE anchored + bounded
|
|
267
|
+
throw new GuardPop3CommandError("guard-pop3-command/bad-msg-number",
|
|
268
|
+
"guardPop3Command.validate: " + verb + " requires a positive decimal message-number");
|
|
269
|
+
}
|
|
270
|
+
args = [rest];
|
|
271
|
+
break;
|
|
272
|
+
case "TOP":
|
|
273
|
+
// `TOP msg n` — message + non-negative line-count.
|
|
274
|
+
var topParts = rest.split(" ");
|
|
275
|
+
if (topParts.length !== 2 ||
|
|
276
|
+
!MSG_NUM_RE.test(topParts[0]) || // allow:regex-no-length-cap — MSG_NUM_RE anchored + bounded
|
|
277
|
+
!/^[0-9]{1,10}$/.test(topParts[1])) { // allow:regex-no-length-cap — anchored + bounded line-count
|
|
278
|
+
throw new GuardPop3CommandError("guard-pop3-command/bad-top",
|
|
279
|
+
"guardPop3Command.validate: TOP requires `msg-num line-count` (both decimal)");
|
|
280
|
+
}
|
|
281
|
+
args = topParts;
|
|
282
|
+
break;
|
|
283
|
+
default:
|
|
284
|
+
// STLS / CAPA / STAT / NOOP / RSET / QUIT — ZERO_ARG_VERBS guard
|
|
285
|
+
// above already enforced no-args. Empty args.
|
|
286
|
+
args = [];
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { verb: verb, args: args };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* @primitive b.guardPop3Command.compliancePosture
|
|
295
|
+
* @signature b.guardPop3Command.compliancePosture(posture)
|
|
296
|
+
* @since 0.9.52
|
|
297
|
+
* @status stable
|
|
298
|
+
*
|
|
299
|
+
* Return the effective profile for a compliance posture, or `null`
|
|
300
|
+
* for unknown names.
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* b.guardPop3Command.compliancePosture("hipaa"); // → "strict"
|
|
304
|
+
*/
|
|
305
|
+
function compliancePosture(posture) {
|
|
306
|
+
return COMPLIANCE_POSTURES[posture] || null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
module.exports = {
|
|
310
|
+
validate: validate,
|
|
311
|
+
compliancePosture: compliancePosture,
|
|
312
|
+
PROFILES: PROFILES,
|
|
313
|
+
COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
|
|
314
|
+
KNOWN_VERBS: KNOWN_VERBS,
|
|
315
|
+
ZERO_ARG_VERBS: ZERO_ARG_VERBS,
|
|
316
|
+
GuardPop3CommandError: GuardPop3CommandError,
|
|
317
|
+
};
|
|
@@ -259,6 +259,17 @@ function _validateGreeting(verb, rest, caps) {
|
|
|
259
259
|
throw new GuardSmtpCommandError("guard-smtp-command/bad-shape",
|
|
260
260
|
verb + " requires a domain or address literal argument (RFC 5321 §4.1.1.1)");
|
|
261
261
|
}
|
|
262
|
+
// RFC 5321 §4.1.1.1: HELO/EHLO accepts a domain. Real-world MTAs
|
|
263
|
+
// (Postfix, Exim, sendmail) tolerate a single trailing space after
|
|
264
|
+
// the domain — the framework refused it because the DOMAIN_RE
|
|
265
|
+
// doesn't match a domain with trailing whitespace. Strip a single
|
|
266
|
+
// trailing space before the leading-space / double-space check so a
|
|
267
|
+
// legitimate "HELO mail.example.com " passes while abusive multi-
|
|
268
|
+
// space shapes still refuse.
|
|
269
|
+
if (rest.charAt(rest.length - 1) === " " &&
|
|
270
|
+
rest.charAt(rest.length - 2) !== " ") {
|
|
271
|
+
rest = rest.slice(0, -1);
|
|
272
|
+
}
|
|
262
273
|
// Trim trailing-space tolerance — most peers send a single space; we
|
|
263
274
|
// accept it but refuse multiple spaces or leading spaces.
|
|
264
275
|
if (rest.charAt(0) === " " || rest.indexOf(" ") !== -1) {
|
|
@@ -506,9 +517,53 @@ function detectBodySmuggling(buf) {
|
|
|
506
517
|
throw new GuardSmtpCommandError("guard-smtp-command/bad-input",
|
|
507
518
|
"detectBodySmuggling: input must be a Buffer");
|
|
508
519
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
520
|
+
// The CVE-2023-51764 / 51765 / 51766 / 2024-32178 class is any
|
|
521
|
+
// dot-line whose line boundary is anything OTHER than canonical
|
|
522
|
+
// \r\n on BOTH sides of the dot. The canonical-and-only terminator
|
|
523
|
+
// is `\r\n.\r\n`. Every other shape that some receiver might honor
|
|
524
|
+
// is a smuggling vector:
|
|
525
|
+
//
|
|
526
|
+
// shape leading . trailing
|
|
527
|
+
// -------------- ----------- - -------------
|
|
528
|
+
// bare-LF/bare-LF \n . \n ← original detector
|
|
529
|
+
// bare-LF/CRLF \n . \r\n
|
|
530
|
+
// CRLF/bare-LF \r\n . \n ← bare-LF terminator
|
|
531
|
+
// bare-CR/anything \r (no LF) . * ← bare CR (RFC violations)
|
|
532
|
+
//
|
|
533
|
+
// Standalone `.\n` or `\n.\n` at the START of the buffer also
|
|
534
|
+
// count: a dot at byte 0 followed by `\n` would terminate any
|
|
535
|
+
// receiver that accepts bare-LF dot.
|
|
536
|
+
// 0x0a = LF, 0x0d = CR, 0x2e = `.`
|
|
537
|
+
if (buf.length >= 2 && buf[0] === 0x2e && buf[1] === 0x0a) return true;
|
|
538
|
+
// Walk every LF in the buffer. The previous byte must be CR for the
|
|
539
|
+
// line boundary to be canonical; otherwise the line started with
|
|
540
|
+
// bare-LF. If the next bytes are `.` followed by ANY of (LF, CRLF),
|
|
541
|
+
// the shape is a smuggling candidate.
|
|
542
|
+
for (var i = 0; i < buf.length - 1; i += 1) {
|
|
543
|
+
if (buf[i] !== 0x0a) continue;
|
|
544
|
+
var leadingBareLf = (i === 0) || (buf[i - 1] !== 0x0d);
|
|
545
|
+
if (buf[i + 1] !== 0x2e) continue;
|
|
546
|
+
// Trailing terminator shape after the dot:
|
|
547
|
+
// buf[i+2] == LF → bare-LF terminator (always smuggling)
|
|
548
|
+
// buf[i+2] == CR && buf[i+3] == LF → CRLF after dot
|
|
549
|
+
// (only smuggling when the
|
|
550
|
+
// leading boundary was bare-LF)
|
|
551
|
+
if (i + 2 < buf.length && buf[i + 2] === 0x0a) {
|
|
552
|
+
// `.\n` after a bare-LF or CRLF line boundary — both
|
|
553
|
+
// smuggling vectors (CRLF.\n is the v0.9.x-audit case).
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
if (leadingBareLf && i + 3 < buf.length &&
|
|
557
|
+
buf[i + 2] === 0x0d && buf[i + 3] === 0x0a) {
|
|
558
|
+
// bare-LF.\r\n — smuggling shape (CVE-2023-51764 Postfix).
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
// Also check for bare-CR-only dot terminators: `\r.\r` (no LF).
|
|
563
|
+
// Some legacy parsers honor bare CR as line terminator.
|
|
564
|
+
for (var j = 0; j < buf.length - 2; j += 1) {
|
|
565
|
+
if (buf[j] === 0x0d && (j + 1 >= buf.length || buf[j + 1] !== 0x0a) &&
|
|
566
|
+
buf[j + 1] === 0x2e && j + 2 < buf.length && buf[j + 2] === 0x0d) {
|
|
512
567
|
return true;
|
|
513
568
|
}
|
|
514
569
|
}
|
package/lib/mail-agent.js
CHANGED
|
@@ -485,17 +485,30 @@ async function _delete(ctx, args) {
|
|
|
485
485
|
}
|
|
486
486
|
|
|
487
487
|
async function _sievePut(ctx, args) {
|
|
488
|
-
//
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
//
|
|
488
|
+
// Two-stage validation: agent-level shape guard for RBAC + name +
|
|
489
|
+
// size, then the full RFC 5228 grammar parse via b.safeSieve. The
|
|
490
|
+
// grammar parse refuses unknown / not-yet-implemented capabilities
|
|
491
|
+
// at `require` time (RFC 5228 §3.2) so the operator's persistence
|
|
492
|
+
// step never gets a script the framework can't actually execute.
|
|
492
493
|
_entry(ctx, "sieve.put", args);
|
|
493
494
|
guardMailSieve.validate({
|
|
494
495
|
kind: "put", actor: args.actor, name: args.name, script: args.script,
|
|
495
496
|
}, { profile: _profileFor(ctx), posture: ctx.posture, ownedNames: args.ownedNames });
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
497
|
+
var safeSieve = require("./safe-sieve"); // allow:inline-require — lazy-load until first sieve.put call
|
|
498
|
+
var rv = safeSieve.validate(args.script, {
|
|
499
|
+
profile: _profileFor(ctx),
|
|
500
|
+
compliancePosture: ctx.posture,
|
|
501
|
+
});
|
|
502
|
+
if (!rv.ok) {
|
|
503
|
+
throw new MailAgentError("mail-agent/sieve-parse-error",
|
|
504
|
+
"agent.sieve.put: Sieve script refused — " +
|
|
505
|
+
(rv.issues[0] && rv.issues[0].snippet ? rv.issues[0].snippet : "parse failed"));
|
|
506
|
+
}
|
|
507
|
+
ctx.auditEmit("mail.agent.sieve.put", args && args.actor, {
|
|
508
|
+
name: args.name,
|
|
509
|
+
requiredCaps: rv.requiredCaps,
|
|
510
|
+
});
|
|
511
|
+
return { ok: true, requiredCaps: rv.requiredCaps };
|
|
499
512
|
}
|
|
500
513
|
|
|
501
514
|
function _notImplemented(ctx, method, args) {
|
package/lib/mail-arc-sign.js
CHANGED
|
@@ -52,6 +52,7 @@ var nodeCrypto = require("node:crypto");
|
|
|
52
52
|
var lazyRequire = require("./lazy-require");
|
|
53
53
|
var validateOpts = require("./validate-opts");
|
|
54
54
|
var safeBuffer = require("./safe-buffer");
|
|
55
|
+
var dkim = require("./mail-dkim");
|
|
55
56
|
var { defineClass } = require("./framework-error");
|
|
56
57
|
|
|
57
58
|
var MailAuthError = defineClass("MailAuthError", { alwaysPermanent: true });
|
|
@@ -107,20 +108,23 @@ function _canonRelaxedHeader(name, value) {
|
|
|
107
108
|
return name.toLowerCase() + ":" + trimmed + "\r\n";
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
// RFC 8617 §5.1.1 references RFC 6376 §3.4.4 for body canonicalization.
|
|
112
|
+
// The DKIM verifier and signer share `_canonBodyRelaxed`; ARC MUST
|
|
113
|
+
// produce a byte-identical canon so a downstream ARC-verifier (which
|
|
114
|
+
// composes the DKIM verifier per §5.1.1) reaches the same body hash.
|
|
115
|
+
// Earlier inline shape collapsed `[ \t]+` across newlines (the regex
|
|
116
|
+
// is global and not bound per-line), which diverged from DKIM's
|
|
117
|
+
// per-line `safeBuffer.stripTrailingHspace` on a line whose only WSP
|
|
118
|
+
// run sat at the end. Compose the DKIM canon directly.
|
|
110
119
|
function _canonRelaxedBody(body) {
|
|
111
|
-
|
|
112
|
-
// trailing WSP, remove all trailing empty lines, append single CRLF
|
|
113
|
-
// unless body is empty.
|
|
114
|
-
if (body.length === 0) return "";
|
|
115
|
-
var normalized = body.replace(/\r?\n/g, "\r\n");
|
|
116
|
-
var collapsed = normalized.replace(/[ \t]+/g, " ").replace(/[ \t]+\r\n/g, "\r\n");
|
|
117
|
-
collapsed = collapsed.replace(/(\r\n)+$/, "");
|
|
118
|
-
return collapsed + "\r\n";
|
|
120
|
+
return dkim._canonBodyRelaxedForTest(body || "");
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
function _bodyHashB64(body, algorithm) {
|
|
122
124
|
var hashAlgo = algorithm.indexOf("sha256") !== -1 ? "sha256" : "sha512";
|
|
123
125
|
var canonical = _canonRelaxedBody(body);
|
|
126
|
+
// RFC 6376 §3.4.4 — empty body canon is `\r\n` (one CRLF). Hash
|
|
127
|
+
// includes that CRLF.
|
|
124
128
|
return nodeCrypto.createHash(hashAlgo).update(canonical).digest("base64");
|
|
125
129
|
}
|
|
126
130
|
|