@blamejs/core 0.9.49 → 0.10.2
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 +952 -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 +78 -5
- package/lib/circuit-breaker.js +10 -2
- package/lib/cli.js +13 -0
- 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-graphql.js +37 -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-regex.js +138 -1
- package/lib/guard-smtp-command.js +58 -3
- package/lib/guard-xml.js +39 -1
- 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/otel-export.js +13 -4
- 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
|
+
};
|
package/lib/guard-regex.js
CHANGED
|
@@ -70,6 +70,14 @@ var BOUNDED_REPEAT_RE = /\{(\d+)(?:,(\d*))?\}/g;
|
|
|
70
70
|
// Lookaround with internal quantifier — `(?=.*+)`, `(?!a*)`.
|
|
71
71
|
var LOOKAROUND_QUANT_RE = /\(\?[=!<][^()]*[*+]/;
|
|
72
72
|
|
|
73
|
+
// Nested extglob detector — picomatch `*(...)` / `+(...)` / `?(...)` /
|
|
74
|
+
// `@(...)` / `!(...)` containing another extglob inside (CVE-2026-33671
|
|
75
|
+
// nested-extglob catastrophic-backtracking class). Two extglob heads in
|
|
76
|
+
// the same pattern with no closing paren between them indicates nesting.
|
|
77
|
+
// The consecutive-star detector (CVE-2026-26996) walks the input by
|
|
78
|
+
// char so doesn't need a regex literal.
|
|
79
|
+
var EXTGLOB_HEAD_RE = /[*+?@!]\(/g; // allow:regex-no-length-cap — input bounded by maxPatternBytes
|
|
80
|
+
|
|
73
81
|
// ---- Profile presets ----
|
|
74
82
|
|
|
75
83
|
var PROFILES = Object.freeze({
|
|
@@ -82,7 +90,11 @@ var PROFILES = Object.freeze({
|
|
|
82
90
|
alternationQuantPolicy: "reject",
|
|
83
91
|
boundedRepeatPolicy: "reject",
|
|
84
92
|
lookaroundQuantPolicy: "reject",
|
|
93
|
+
consecutiveStarPolicy: "reject",
|
|
94
|
+
nestedExtglobPolicy: "reject",
|
|
95
|
+
inputKind: "regex", // CVE-2026-26996 + CVE-2026-33671 detectors apply only when inputKind=="glob"
|
|
85
96
|
maxBoundedRepeat: 100, // allow:raw-byte-literal — bounded repeat ceiling
|
|
97
|
+
maxConsecutiveStars: 2, // allow:raw-byte-literal — `**` recursive glob permitted; >=3 refused
|
|
86
98
|
maxPatternBytes: C.BYTES.kib(1),
|
|
87
99
|
maxBytes: C.BYTES.kib(1),
|
|
88
100
|
maxRuntimeMs: C.TIME.seconds(2),
|
|
@@ -96,7 +108,10 @@ var PROFILES = Object.freeze({
|
|
|
96
108
|
alternationQuantPolicy: "audit",
|
|
97
109
|
boundedRepeatPolicy: "audit",
|
|
98
110
|
lookaroundQuantPolicy: "audit",
|
|
111
|
+
consecutiveStarPolicy: "reject", // CVE-2026-26996 refused at every profile
|
|
112
|
+
nestedExtglobPolicy: "reject", // CVE-2026-33671 refused at every profile
|
|
99
113
|
maxBoundedRepeat: 1000, // allow:raw-byte-literal — bounded repeat ceiling
|
|
114
|
+
maxConsecutiveStars: 2, // allow:raw-byte-literal — `**` recursive glob permitted; >=3 refused
|
|
100
115
|
maxPatternBytes: C.BYTES.kib(2),
|
|
101
116
|
maxBytes: C.BYTES.kib(2),
|
|
102
117
|
maxRuntimeMs: C.TIME.seconds(2),
|
|
@@ -110,7 +125,10 @@ var PROFILES = Object.freeze({
|
|
|
110
125
|
alternationQuantPolicy: "allow",
|
|
111
126
|
boundedRepeatPolicy: "audit",
|
|
112
127
|
lookaroundQuantPolicy: "audit",
|
|
128
|
+
consecutiveStarPolicy: "reject", // CVE-2026-26996 refused at every profile
|
|
129
|
+
nestedExtglobPolicy: "reject", // CVE-2026-33671 refused at every profile
|
|
113
130
|
maxBoundedRepeat: 10000, // allow:raw-byte-literal — bounded repeat ceiling
|
|
131
|
+
maxConsecutiveStars: 2, // allow:raw-byte-literal — `**` recursive glob permitted; >=3 refused
|
|
114
132
|
maxPatternBytes: C.BYTES.kib(8),
|
|
115
133
|
maxBytes: C.BYTES.kib(8),
|
|
116
134
|
maxRuntimeMs: C.TIME.seconds(2),
|
|
@@ -223,9 +241,116 @@ function _detectIssues(input, opts) {
|
|
|
223
241
|
}
|
|
224
242
|
}
|
|
225
243
|
|
|
244
|
+
_detectConsecutiveStar(input, opts, issues);
|
|
245
|
+
_detectNestedExtglob(input, opts, issues);
|
|
246
|
+
|
|
226
247
|
return issues;
|
|
227
248
|
}
|
|
228
249
|
|
|
250
|
+
// Consecutive-star wildcard cap (CVE-2026-26996). Operator-supplied
|
|
251
|
+
// glob fragments compile to picomatch / RegExp; a long run of `*`
|
|
252
|
+
// against a non-matching literal walks O(4^N). Three-or-more
|
|
253
|
+
// consecutive `*` is the canonical bad shape; `**` (recursive glob)
|
|
254
|
+
// stays permitted, gated by the profile's `maxConsecutiveStars`.
|
|
255
|
+
function _detectConsecutiveStar(input, opts, issues) {
|
|
256
|
+
if (opts.consecutiveStarPolicy === "allow") return;
|
|
257
|
+
// CVE-2026-26996 is a picomatch / glob-shape backtracking class —
|
|
258
|
+
// `***+literal` walks O(4^N) when picomatch translates the run to a
|
|
259
|
+
// backtracking-heavy regex. Native ECMAScript regex syntax cannot
|
|
260
|
+
// produce three consecutive `*` quantifiers (it's a SyntaxError),
|
|
261
|
+
// so applying this detector to `inputKind: "regex"` strings only
|
|
262
|
+
// produces false positives on legitimate regex shapes like
|
|
263
|
+
// `a*(b)*` where `*(` is quantifier+group, not extglob.
|
|
264
|
+
if (opts.inputKind !== "glob") return;
|
|
265
|
+
var starRun = 0;
|
|
266
|
+
var starRunMax = 0;
|
|
267
|
+
for (var si = 0; si < input.length; si += 1) {
|
|
268
|
+
if (input.charAt(si) === "*") {
|
|
269
|
+
starRun += 1;
|
|
270
|
+
if (starRun > starRunMax) starRunMax = starRun;
|
|
271
|
+
} else {
|
|
272
|
+
starRun = 0;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
var starCeiling = opts.maxConsecutiveStars === undefined ?
|
|
276
|
+
2 : opts.maxConsecutiveStars; // allow:raw-byte-literal — `**` glob ceiling
|
|
277
|
+
if (starRunMax > starCeiling) {
|
|
278
|
+
issues.push({
|
|
279
|
+
kind: "consecutive-star",
|
|
280
|
+
severity: opts.consecutiveStarPolicy === "reject" ? "critical" : "high",
|
|
281
|
+
ruleId: "regex.consecutive-star",
|
|
282
|
+
snippet: "pattern has " + starRunMax + " consecutive `*` " +
|
|
283
|
+
"wildcards (cap " + starCeiling + ") — O(4^N) " +
|
|
284
|
+
"backtracking on non-matching literal (CVE-2026-26996)",
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Nested-extglob detector (CVE-2026-33671). picomatch `*(...)` /
|
|
290
|
+
// `+(...)` / `?(...)` / `@(...)` / `!(...)` containing another
|
|
291
|
+
// extglob inside compiles to catastrophic-backtracking regex.
|
|
292
|
+
function _detectNestedExtglob(input, opts, issues) {
|
|
293
|
+
if (opts.nestedExtglobPolicy === "allow") return;
|
|
294
|
+
// CVE-2026-33671 is picomatch-specific: the extglob heads `*(`/
|
|
295
|
+
// `+(`/`?(`/`@(`/`!(` collide with valid ECMAScript regex shapes
|
|
296
|
+
// (quantifier + capturing group). Restricting this detector to
|
|
297
|
+
// `inputKind: "glob"` avoids false-positive refusal of regex
|
|
298
|
+
// patterns like `a*(b+(c))` where the heads are quantifier
|
|
299
|
+
// groupings, not extglob.
|
|
300
|
+
if (opts.inputKind !== "glob") return;
|
|
301
|
+
// Collect extglob head positions via match() — read-only scan.
|
|
302
|
+
var heads = [];
|
|
303
|
+
var allHeads = input.match(EXTGLOB_HEAD_RE); // allow:regex-no-length-cap — input bounded by maxPatternBytes
|
|
304
|
+
if (allHeads === null || allHeads.length < 2) return;
|
|
305
|
+
// Locate each head index manually (match returns substrings, not idx).
|
|
306
|
+
var scanFrom = 0;
|
|
307
|
+
for (var hh = 0; hh < allHeads.length; hh += 1) {
|
|
308
|
+
var ch0 = allHeads[hh].charAt(0);
|
|
309
|
+
var idx = scanFrom;
|
|
310
|
+
while (idx < input.length - 1) {
|
|
311
|
+
var c0 = input.charAt(idx);
|
|
312
|
+
var c1 = input.charAt(idx + 1);
|
|
313
|
+
if (c1 === "(" && c0 === ch0) break;
|
|
314
|
+
idx += 1;
|
|
315
|
+
}
|
|
316
|
+
heads.push(idx);
|
|
317
|
+
scanFrom = idx + 1;
|
|
318
|
+
if (heads.length > 1024) break; // allow:raw-byte-literal — head-count safety cap
|
|
319
|
+
}
|
|
320
|
+
var nested = false;
|
|
321
|
+
for (var hi = 0; hi < heads.length && !nested; hi += 1) {
|
|
322
|
+
var headStart = heads[hi];
|
|
323
|
+
// Walk forward tracking paren depth. Inner head before close = nested.
|
|
324
|
+
var pdepth = 1;
|
|
325
|
+
for (var pj = headStart + 2; pj < input.length && pdepth > 0; pj += 1) {
|
|
326
|
+
var ch = input.charAt(pj);
|
|
327
|
+
if (ch === "(") {
|
|
328
|
+
pdepth += 1;
|
|
329
|
+
if (pj > 0) {
|
|
330
|
+
var preVerb = input.charAt(pj - 1);
|
|
331
|
+
if (preVerb === "*" || preVerb === "+" || preVerb === "?" ||
|
|
332
|
+
preVerb === "@" || preVerb === "!") {
|
|
333
|
+
nested = true;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} else if (ch === ")") {
|
|
338
|
+
pdepth -= 1;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (nested) {
|
|
343
|
+
issues.push({
|
|
344
|
+
kind: "nested-extglob",
|
|
345
|
+
severity: opts.nestedExtglobPolicy === "reject" ? "critical" : "high",
|
|
346
|
+
ruleId: "regex.nested-extglob",
|
|
347
|
+
snippet: "pattern contains nested extglob quantifier " +
|
|
348
|
+
"(`*(...*(...))`) — catastrophic backtracking class " +
|
|
349
|
+
"(CVE-2026-33671 picomatch)",
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
229
354
|
/**
|
|
230
355
|
* @primitive b.guardRegex.validate
|
|
231
356
|
* @signature b.guardRegex.validate(input, opts)
|
|
@@ -252,7 +377,11 @@ function _detectIssues(input, opts) {
|
|
|
252
377
|
* alternationQuantPolicy: "reject"|"audit"|"allow",
|
|
253
378
|
* boundedRepeatPolicy: "reject"|"audit"|"allow",
|
|
254
379
|
* lookaroundQuantPolicy: "reject"|"audit"|"allow",
|
|
380
|
+
* consecutiveStarPolicy: "reject"|"audit"|"allow",
|
|
381
|
+
* nestedExtglobPolicy: "reject"|"audit"|"allow",
|
|
382
|
+
* inputKind: "regex"|"glob",
|
|
255
383
|
* maxBoundedRepeat: number,
|
|
384
|
+
* maxConsecutiveStars: number,
|
|
256
385
|
* maxPatternBytes: number,
|
|
257
386
|
* maxBytes: number,
|
|
258
387
|
* maxRuntimeMs: number,
|
|
@@ -268,7 +397,7 @@ function _detectIssues(input, opts) {
|
|
|
268
397
|
function validate(input, opts) {
|
|
269
398
|
opts = _resolveOpts(opts);
|
|
270
399
|
numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
|
|
271
|
-
["maxBytes", "maxPatternBytes", "maxBoundedRepeat"],
|
|
400
|
+
["maxBytes", "maxPatternBytes", "maxBoundedRepeat", "maxConsecutiveStars"],
|
|
272
401
|
"guardRegex.validate", GuardRegexError, "regex.bad-opt");
|
|
273
402
|
return gateContract.aggregateIssues(_detectIssues(input, opts));
|
|
274
403
|
}
|
|
@@ -298,7 +427,11 @@ function validate(input, opts) {
|
|
|
298
427
|
* alternationQuantPolicy: "reject"|"audit"|"allow",
|
|
299
428
|
* boundedRepeatPolicy: "reject"|"audit"|"allow",
|
|
300
429
|
* lookaroundQuantPolicy: "reject"|"audit"|"allow",
|
|
430
|
+
* consecutiveStarPolicy: "reject"|"audit"|"allow",
|
|
431
|
+
* nestedExtglobPolicy: "reject"|"audit"|"allow",
|
|
432
|
+
* inputKind: "regex"|"glob",
|
|
301
433
|
* maxBoundedRepeat: number,
|
|
434
|
+
* maxConsecutiveStars: number,
|
|
302
435
|
* maxPatternBytes: number,
|
|
303
436
|
*
|
|
304
437
|
* @example
|
|
@@ -350,7 +483,11 @@ function sanitize(input, opts) {
|
|
|
350
483
|
* alternationQuantPolicy: "reject"|"audit"|"allow",
|
|
351
484
|
* boundedRepeatPolicy: "reject"|"audit"|"allow",
|
|
352
485
|
* lookaroundQuantPolicy: "reject"|"audit"|"allow",
|
|
486
|
+
* consecutiveStarPolicy: "reject"|"audit"|"allow",
|
|
487
|
+
* nestedExtglobPolicy: "reject"|"audit"|"allow",
|
|
488
|
+
* inputKind: "regex"|"glob",
|
|
353
489
|
* maxBoundedRepeat: number,
|
|
490
|
+
* maxConsecutiveStars: number,
|
|
354
491
|
* maxPatternBytes: number,
|
|
355
492
|
*
|
|
356
493
|
* @example
|
|
@@ -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
|
}
|