@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +952 -908
  2. package/index.js +25 -0
  3. package/lib/_test/crypto-fixtures.js +67 -0
  4. package/lib/agent-event-bus.js +52 -6
  5. package/lib/agent-idempotency.js +169 -16
  6. package/lib/agent-orchestrator.js +263 -9
  7. package/lib/agent-posture-chain.js +163 -5
  8. package/lib/agent-saga.js +146 -16
  9. package/lib/agent-snapshot.js +349 -19
  10. package/lib/agent-stream.js +34 -2
  11. package/lib/agent-tenant.js +179 -23
  12. package/lib/agent-trace.js +84 -21
  13. package/lib/auth/aal.js +8 -1
  14. package/lib/auth/ciba.js +6 -1
  15. package/lib/auth/dpop.js +7 -2
  16. package/lib/auth/fal.js +17 -8
  17. package/lib/auth/jwt-external.js +128 -4
  18. package/lib/auth/oauth.js +232 -10
  19. package/lib/auth/oid4vci.js +67 -7
  20. package/lib/auth/openid-federation.js +71 -25
  21. package/lib/auth/passkey.js +140 -6
  22. package/lib/auth/sd-jwt-vc.js +78 -5
  23. package/lib/circuit-breaker.js +10 -2
  24. package/lib/cli.js +13 -0
  25. package/lib/compliance.js +176 -8
  26. package/lib/crypto-field.js +114 -14
  27. package/lib/crypto.js +216 -20
  28. package/lib/db.js +1 -0
  29. package/lib/guard-graphql.js +37 -0
  30. package/lib/guard-jmap.js +321 -0
  31. package/lib/guard-managesieve-command.js +566 -0
  32. package/lib/guard-pop3-command.js +317 -0
  33. package/lib/guard-regex.js +138 -1
  34. package/lib/guard-smtp-command.js +58 -3
  35. package/lib/guard-xml.js +39 -1
  36. package/lib/mail-agent.js +20 -7
  37. package/lib/mail-arc-sign.js +12 -8
  38. package/lib/mail-auth.js +323 -34
  39. package/lib/mail-crypto-pgp.js +934 -0
  40. package/lib/mail-crypto-smime.js +340 -0
  41. package/lib/mail-crypto.js +108 -0
  42. package/lib/mail-dav.js +1224 -0
  43. package/lib/mail-deploy.js +492 -0
  44. package/lib/mail-dkim.js +431 -26
  45. package/lib/mail-journal.js +435 -0
  46. package/lib/mail-scan.js +502 -0
  47. package/lib/mail-server-imap.js +64 -26
  48. package/lib/mail-server-jmap.js +488 -0
  49. package/lib/mail-server-managesieve.js +853 -0
  50. package/lib/mail-server-mx.js +40 -30
  51. package/lib/mail-server-pop3.js +836 -0
  52. package/lib/mail-server-rate-limit.js +13 -0
  53. package/lib/mail-server-submission.js +70 -24
  54. package/lib/mail-server-tls.js +445 -0
  55. package/lib/mail-sieve.js +557 -0
  56. package/lib/mail-spam-score.js +284 -0
  57. package/lib/mail.js +99 -0
  58. package/lib/metrics.js +80 -3
  59. package/lib/middleware/dpop.js +58 -3
  60. package/lib/middleware/idempotency-key.js +255 -42
  61. package/lib/middleware/protected-resource-metadata.js +114 -2
  62. package/lib/network-dns-resolver.js +33 -0
  63. package/lib/network-tls.js +46 -0
  64. package/lib/otel-export.js +13 -4
  65. package/lib/outbox.js +62 -12
  66. package/lib/pqc-agent.js +13 -5
  67. package/lib/retry.js +23 -9
  68. package/lib/router.js +23 -1
  69. package/lib/safe-ical.js +634 -0
  70. package/lib/safe-icap.js +502 -0
  71. package/lib/safe-mime.js +15 -0
  72. package/lib/safe-sieve.js +684 -0
  73. package/lib/safe-smtp.js +57 -0
  74. package/lib/safe-url.js +37 -0
  75. package/lib/safe-vcard.js +473 -0
  76. package/lib/self-update-standalone-verifier.js +32 -3
  77. package/lib/self-update.js +153 -33
  78. package/lib/vendor/MANIFEST.json +161 -156
  79. package/lib/vendor-data.js +127 -9
  80. package/lib/vex.js +324 -59
  81. package/package.json +1 -1
  82. 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
+ };
@@ -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
- for (var i = 1; i < buf.length - 2; i += 1) {
510
- if (buf[i] === 0x0a /* LF */ && buf[i - 1] !== 0x0d /* CR */ &&
511
- buf[i + 1] === 0x2e /* . */ && buf[i + 2] === 0x0a /* LF */) {
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
  }