@blamejs/core 0.9.18 → 0.9.20
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 +2 -0
- package/index.js +16 -0
- package/lib/guard-mail-compose.js +282 -0
- package/lib/guard-mail-move.js +202 -0
- package/lib/guard-mail-query.js +296 -0
- package/lib/guard-mail-reply.js +172 -0
- package/lib/guard-mail-sieve.js +207 -0
- package/lib/guard-message-id.js +241 -0
- package/lib/mail-agent.js +638 -0
- package/lib/mail-store.js +652 -0
- package/lib/mail.js +6 -0
- package/lib/safe-mime.js +714 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.guardMessageId
|
|
4
|
+
* @nav Guards
|
|
5
|
+
* @title Guard Message-Id
|
|
6
|
+
* @order 420
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* RFC 5322 §3.6.4 Message-Id validator. Gates Message-Id /
|
|
10
|
+
* In-Reply-To / References header values at the entry to
|
|
11
|
+
* `b.mailStore.appendMessage` (v0.9.19), `b.mail.server.mx` (v0.9.23),
|
|
12
|
+
* and the outbound submission path (v0.9.25).
|
|
13
|
+
*
|
|
14
|
+
* Refuses:
|
|
15
|
+
*
|
|
16
|
+
* - oversized (default 998-byte cap per RFC 5322 §2.1.1 line cap)
|
|
17
|
+
* - bare CR / LF / NUL / C0 control chars (header-injection
|
|
18
|
+
* defense — defends `From:` / `Bcc:` smuggling via folded
|
|
19
|
+
* Message-Id continuation)
|
|
20
|
+
* - DEL (0x7F) anywhere
|
|
21
|
+
* - unbracketed under `strict` profile (the wire form per RFC
|
|
22
|
+
* 5322 §3.6.4 is `<unique-token@domain>` — operator with
|
|
23
|
+
* legacy mail can opt down to `balanced` to accept bare tokens)
|
|
24
|
+
* - empty value
|
|
25
|
+
* - bidi codepoints in the local-part / domain (RFC 5322 + EAI
|
|
26
|
+
* allow non-ASCII per RFC 6532 + RFC 5335 but bidi-marker
|
|
27
|
+
* codepoints are operator-unfriendly and refused outright)
|
|
28
|
+
*
|
|
29
|
+
* Profile vocabulary follows the existing guard-family convention:
|
|
30
|
+
*
|
|
31
|
+
* - `strict` (default) — bracketed `<token@domain>`, length cap,
|
|
32
|
+
* no control chars, no bidi
|
|
33
|
+
* - `balanced` — accepts unbracketed tokens (legacy mail compat)
|
|
34
|
+
* - `permissive` — minimal validation (NUL + CR/LF refused; rest
|
|
35
|
+
* passes); use only for forensic-only flows
|
|
36
|
+
*
|
|
37
|
+
* Posture vocabulary:
|
|
38
|
+
*
|
|
39
|
+
* - `hipaa` / `pci-dss` / `gdpr` / `soc2` — each pins the
|
|
40
|
+
* active profile to `strict` regardless of operator's profile
|
|
41
|
+
* opt; refuses to relax under regulated postures.
|
|
42
|
+
*
|
|
43
|
+
* Composes the framework's existing guard-family pattern via
|
|
44
|
+
* `b.gateContract` (the same shape `b.guardEmail` / `b.guardCsv` /
|
|
45
|
+
* `b.guardArchive` use). Registers in `b.guardAll`'s
|
|
46
|
+
* `STANDALONE_GUARDS` map.
|
|
47
|
+
*
|
|
48
|
+
* @card
|
|
49
|
+
* RFC 5322 §3.6.4 Message-Id validator — bounded length, no CRLF/NUL/control chars, bracketed shape under strict profile. Gates header-injection at the mail-store / MX / submission entry points.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
var { defineClass } = require("./framework-error");
|
|
53
|
+
|
|
54
|
+
var GuardMessageIdError = defineClass("GuardMessageIdError", { alwaysPermanent: true });
|
|
55
|
+
|
|
56
|
+
var DEFAULT_PROFILE = "strict";
|
|
57
|
+
|
|
58
|
+
var PROFILES = Object.freeze({
|
|
59
|
+
strict: { requireBrackets: true, maxBytes: 998 }, // allow:raw-byte-literal
|
|
60
|
+
balanced: { requireBrackets: false, maxBytes: 998 }, // allow:raw-byte-literal
|
|
61
|
+
permissive: { requireBrackets: false, maxBytes: 4096 }, // allow:raw-byte-literal — permissive cap, not bytes-as-storage
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
var COMPLIANCE_POSTURES = Object.freeze({
|
|
65
|
+
hipaa: "strict",
|
|
66
|
+
"pci-dss": "strict",
|
|
67
|
+
gdpr: "strict",
|
|
68
|
+
soc2: "strict",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Bidi codepoints refused — same set the framework's address-bidi
|
|
72
|
+
// defense uses (RFC 5322 §3.6.4 doesn't speak EAI codepoints, but RTL
|
|
73
|
+
// codepoints in Message-Ids are operator-unfriendly + defend the
|
|
74
|
+
// CVE-2021-42574 RTLO class in mail header context).
|
|
75
|
+
var BIDI_RE = /[--]/;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @primitive b.guardMessageId.validate
|
|
79
|
+
* @signature b.guardMessageId.validate(value, opts?)
|
|
80
|
+
* @since 0.9.19
|
|
81
|
+
* @status stable
|
|
82
|
+
* @related b.guardMessageId.validateList, b.safeMime.parse, b.guardEmail
|
|
83
|
+
*
|
|
84
|
+
* Validate a Message-Id / In-Reply-To / References header value.
|
|
85
|
+
* Returns the input value on success; throws `GuardMessageIdError`
|
|
86
|
+
* on refusal.
|
|
87
|
+
*
|
|
88
|
+
* @opts
|
|
89
|
+
* profile: "strict" | "balanced" | "permissive", // default "strict"
|
|
90
|
+
* posture: "hipaa" | "pci-dss" | "gdpr" | "soc2", // pins profile to strict
|
|
91
|
+
* maxBytes: number, // per-profile default
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* b.guardMessageId.validate("<abc@example.com>");
|
|
95
|
+
* // → "<abc@example.com>"
|
|
96
|
+
*
|
|
97
|
+
* try { b.guardMessageId.validate("abc@example.com"); }
|
|
98
|
+
* catch (e) { e.code; }
|
|
99
|
+
* // → "message-id/unbracketed" (strict profile)
|
|
100
|
+
*/
|
|
101
|
+
function validate(value, opts) {
|
|
102
|
+
opts = opts || {};
|
|
103
|
+
var profileName = _resolveProfile(opts);
|
|
104
|
+
var profile = PROFILES[profileName];
|
|
105
|
+
var maxBytes = typeof opts.maxBytes === "number" ? opts.maxBytes : profile.maxBytes;
|
|
106
|
+
|
|
107
|
+
if (typeof value !== "string") {
|
|
108
|
+
throw new GuardMessageIdError("message-id/bad-input",
|
|
109
|
+
"guardMessageId.validate: value must be a string (got " + typeof value + ")");
|
|
110
|
+
}
|
|
111
|
+
if (value.length === 0) {
|
|
112
|
+
throw new GuardMessageIdError("message-id/empty",
|
|
113
|
+
"guardMessageId.validate: empty Message-Id refused");
|
|
114
|
+
}
|
|
115
|
+
if (Buffer.byteLength(value, "utf8") > maxBytes) {
|
|
116
|
+
throw new GuardMessageIdError("message-id/oversize",
|
|
117
|
+
"guardMessageId.validate: " + Buffer.byteLength(value, "utf8") +
|
|
118
|
+
" bytes exceeds maxBytes=" + maxBytes + " (RFC 5322 §2.1.1)");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// C0 control chars + NUL + DEL — always refused at every profile
|
|
122
|
+
// (defends mail-header-injection class — operator can't smuggle
|
|
123
|
+
// CR/LF into a Message-Id to fold an attacker-chosen From: line).
|
|
124
|
+
for (var i = 0; i < value.length; i += 1) {
|
|
125
|
+
var c = value.charCodeAt(i);
|
|
126
|
+
if (c < 0x20 || c === 0x7F) { // allow:raw-byte-literal — C0 + DEL refusal
|
|
127
|
+
throw new GuardMessageIdError("message-id/control-char",
|
|
128
|
+
"guardMessageId.validate: control char 0x" + c.toString(16) + " at offset " + i);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Bidi codepoints — refused at strict + balanced; permissive lets
|
|
133
|
+
// them through. Length-bounded by the maxBytes check above so a
|
|
134
|
+
// hostile input can't burn regex-engine CPU; the bidi codepoint set
|
|
135
|
+
// is tiny so the test is constant-time anyway.
|
|
136
|
+
if (profileName !== "permissive" && BIDI_RE.test(value)) { // allow:regex-no-length-cap — value length-bounded by Buffer.byteLength check above
|
|
137
|
+
throw new GuardMessageIdError("message-id/bidi",
|
|
138
|
+
"guardMessageId.validate: bidi codepoint refused (CVE-2021-42574 RTLO class in mail-header context)");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Bracketed shape — required under strict.
|
|
142
|
+
if (profile.requireBrackets) {
|
|
143
|
+
if (value.charAt(0) !== "<" || value.charAt(value.length - 1) !== ">") {
|
|
144
|
+
throw new GuardMessageIdError("message-id/unbracketed",
|
|
145
|
+
"guardMessageId.validate: strict profile requires `<token@domain>` shape (RFC 5322 §3.6.4)");
|
|
146
|
+
}
|
|
147
|
+
var inner = value.slice(1, -1);
|
|
148
|
+
var at = inner.indexOf("@");
|
|
149
|
+
if (at <= 0 || at === inner.length - 1) {
|
|
150
|
+
throw new GuardMessageIdError("message-id/no-at",
|
|
151
|
+
"guardMessageId.validate: Message-Id must contain `@` between local-part and domain");
|
|
152
|
+
}
|
|
153
|
+
if (inner.indexOf("<") >= 0 || inner.indexOf(">") >= 0) {
|
|
154
|
+
throw new GuardMessageIdError("message-id/nested-brackets",
|
|
155
|
+
"guardMessageId.validate: nested angle brackets refused");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @primitive b.guardMessageId.validateList
|
|
164
|
+
* @signature b.guardMessageId.validateList(value, opts?)
|
|
165
|
+
* @since 0.9.19
|
|
166
|
+
* @status stable
|
|
167
|
+
* @related b.guardMessageId.validate
|
|
168
|
+
*
|
|
169
|
+
* Validate a Message-Id-list header value (References / In-Reply-To
|
|
170
|
+
* may carry multiple ids separated by whitespace per RFC 5322 §3.6.4).
|
|
171
|
+
* Returns the array of validated Message-Ids; throws on any single
|
|
172
|
+
* refusal.
|
|
173
|
+
*
|
|
174
|
+
* @opts
|
|
175
|
+
* profile: same as validate
|
|
176
|
+
* posture: same as validate
|
|
177
|
+
* maxBytes: per-id cap
|
|
178
|
+
* maxIds: number, // default 100 — References-chain cap
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* b.guardMessageId.validateList("<a@x> <b@x> <c@x>");
|
|
182
|
+
* // → ["<a@x>", "<b@x>", "<c@x>"]
|
|
183
|
+
*/
|
|
184
|
+
function validateList(value, opts) {
|
|
185
|
+
opts = opts || {};
|
|
186
|
+
var maxIds = typeof opts.maxIds === "number" ? opts.maxIds : 100; // allow:raw-byte-literal — References-chain cap, not bytes
|
|
187
|
+
if (typeof value !== "string") {
|
|
188
|
+
throw new GuardMessageIdError("message-id/bad-input",
|
|
189
|
+
"guardMessageId.validateList: value must be a string");
|
|
190
|
+
}
|
|
191
|
+
var ids = value.split(/\s+/).filter(function (s) { return s.length > 0; });
|
|
192
|
+
if (ids.length > maxIds) {
|
|
193
|
+
throw new GuardMessageIdError("message-id/chain-too-long",
|
|
194
|
+
"guardMessageId.validateList: " + ids.length + " ids exceeds maxIds=" + maxIds);
|
|
195
|
+
}
|
|
196
|
+
for (var i = 0; i < ids.length; i += 1) {
|
|
197
|
+
validate(ids[i], opts);
|
|
198
|
+
}
|
|
199
|
+
return ids;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @primitive b.guardMessageId.compliancePosture
|
|
204
|
+
* @signature b.guardMessageId.compliancePosture(posture)
|
|
205
|
+
* @since 0.9.19
|
|
206
|
+
* @status stable
|
|
207
|
+
*
|
|
208
|
+
* Return the effective profile for a given compliance posture.
|
|
209
|
+
* Composed by `b.compliance.set` to surface "what posture is active
|
|
210
|
+
* for which guard" in audit rows.
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* b.guardMessageId.compliancePosture("hipaa"); // → "strict"
|
|
214
|
+
* b.guardMessageId.compliancePosture("unknown"); // → null
|
|
215
|
+
*/
|
|
216
|
+
function compliancePosture(posture) {
|
|
217
|
+
return COMPLIANCE_POSTURES[posture] || null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function _resolveProfile(opts) {
|
|
221
|
+
if (opts.posture && COMPLIANCE_POSTURES[opts.posture]) {
|
|
222
|
+
return COMPLIANCE_POSTURES[opts.posture];
|
|
223
|
+
}
|
|
224
|
+
var p = opts.profile || DEFAULT_PROFILE;
|
|
225
|
+
if (!PROFILES[p]) {
|
|
226
|
+
throw new GuardMessageIdError("message-id/bad-profile",
|
|
227
|
+
"guardMessageId: unknown profile '" + p + "' (use strict / balanced / permissive)");
|
|
228
|
+
}
|
|
229
|
+
return p;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = {
|
|
233
|
+
validate: validate,
|
|
234
|
+
validateList: validateList,
|
|
235
|
+
compliancePosture: compliancePosture,
|
|
236
|
+
PROFILES: PROFILES,
|
|
237
|
+
COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
|
|
238
|
+
GuardMessageIdError: GuardMessageIdError,
|
|
239
|
+
NAME: "messageId",
|
|
240
|
+
KIND: "identifier",
|
|
241
|
+
};
|