@blamejs/core 0.9.24 → 0.9.38
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 +885 -871
- package/index.js +32 -1
- package/lib/agent-audit.js +45 -0
- package/lib/agent-event-bus.js +336 -0
- package/lib/agent-idempotency.js +2 -8
- package/lib/agent-orchestrator.js +2 -8
- package/lib/agent-posture-chain.js +208 -0
- package/lib/agent-saga.js +191 -0
- package/lib/agent-snapshot.js +346 -0
- package/lib/agent-stream.js +2 -8
- package/lib/agent-tenant.js +308 -0
- package/lib/agent-trace.js +218 -0
- package/lib/guard-all.js +1 -0
- package/lib/guard-dsn.js +379 -0
- package/lib/guard-envelope.js +294 -0
- package/lib/guard-event-bus-payload.js +217 -0
- package/lib/guard-event-bus-topic.js +150 -0
- package/lib/guard-posture-chain.js +201 -0
- package/lib/guard-saga-config.js +157 -0
- package/lib/guard-smtp-command.js +484 -0
- package/lib/guard-snapshot-envelope.js +168 -0
- package/lib/guard-tenant-id.js +138 -0
- package/lib/guard-trace-context.js +172 -0
- package/lib/ip-utils.js +102 -0
- package/lib/mail-auth.js +4 -35
- package/lib/mail-greylist.js +448 -0
- package/lib/mail-helo.js +473 -0
- package/lib/mail-rbl.js +392 -0
- package/lib/mail.js +2 -1
- package/lib/network-dns-resolver.js +500 -0
- package/lib/network.js +1 -0
- package/lib/redis-client.js +2 -1
- package/lib/safe-dns.js +665 -0
- package/lib/tracing.js +36 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.guardTenantId
|
|
4
|
+
* @nav Guards
|
|
5
|
+
* @title Guard Tenant Id
|
|
6
|
+
* @order 440
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Tenant-id shape validator. Tenant ids surface in audit log lines,
|
|
10
|
+
* sealed registry rows, derived-key context labels, and routing
|
|
11
|
+
* keys — they have to be ASCII-greppable across the whole framework
|
|
12
|
+
* stack. Refuses:
|
|
13
|
+
*
|
|
14
|
+
* - non-ASCII (NFC + ASCII-only)
|
|
15
|
+
* - path-traversal shapes (`..` / `/` / `\` / NUL / C0 / DEL)
|
|
16
|
+
* - oversized (default 64 bytes)
|
|
17
|
+
* - reserved `ROOT` / `FRAMEWORK` / `*` / empty
|
|
18
|
+
* - leading `.` (hidden-folder shape)
|
|
19
|
+
*
|
|
20
|
+
* @card
|
|
21
|
+
* Validates tenant-id strings. ASCII-only, bounded, no path-
|
|
22
|
+
* traversal, no reserved names.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
var { defineClass } = require("./framework-error");
|
|
26
|
+
|
|
27
|
+
var GuardTenantIdError = defineClass("GuardTenantIdError", { alwaysPermanent: true });
|
|
28
|
+
|
|
29
|
+
var DEFAULT_PROFILE = "strict";
|
|
30
|
+
|
|
31
|
+
var PROFILES = Object.freeze({
|
|
32
|
+
strict: { maxBytes: 64 }, // allow:raw-byte-literal
|
|
33
|
+
balanced: { maxBytes: 128 }, // allow:raw-byte-literal
|
|
34
|
+
permissive: { maxBytes: 512 }, // allow:raw-byte-literal
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
var COMPLIANCE_POSTURES = Object.freeze({
|
|
38
|
+
hipaa: "strict",
|
|
39
|
+
"pci-dss": "strict",
|
|
40
|
+
gdpr: "strict",
|
|
41
|
+
soc2: "strict",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
var RESERVED = Object.freeze({ "ROOT": true, "FRAMEWORK": true, "*": true });
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @primitive b.guardTenantId.validate
|
|
48
|
+
* @signature b.guardTenantId.validate(tenantId, opts?)
|
|
49
|
+
* @since 0.9.26
|
|
50
|
+
* @status stable
|
|
51
|
+
* @related b.agent.tenant.create
|
|
52
|
+
*
|
|
53
|
+
* Validate a tenant-id string. Returns the id on success; throws
|
|
54
|
+
* `GuardTenantIdError` on refusal.
|
|
55
|
+
*
|
|
56
|
+
* @opts
|
|
57
|
+
* profile: "strict" | "balanced" | "permissive",
|
|
58
|
+
* posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* b.guardTenantId.validate("acme-clinic");
|
|
62
|
+
*/
|
|
63
|
+
function validate(tenantId, opts) {
|
|
64
|
+
opts = opts || {};
|
|
65
|
+
var profile = PROFILES[_resolveProfile(opts)];
|
|
66
|
+
if (typeof tenantId !== "string" || tenantId.length === 0) {
|
|
67
|
+
throw new GuardTenantIdError("tenant-id/bad-input",
|
|
68
|
+
"guardTenantId.validate: tenantId must be a non-empty string");
|
|
69
|
+
}
|
|
70
|
+
if (Buffer.byteLength(tenantId, "utf8") > profile.maxBytes) {
|
|
71
|
+
throw new GuardTenantIdError("tenant-id/oversize",
|
|
72
|
+
"guardTenantId.validate: tenantId exceeds maxBytes=" + profile.maxBytes);
|
|
73
|
+
}
|
|
74
|
+
if (RESERVED[tenantId]) {
|
|
75
|
+
throw new GuardTenantIdError("tenant-id/reserved",
|
|
76
|
+
"guardTenantId.validate: tenantId '" + tenantId + "' is framework-reserved");
|
|
77
|
+
}
|
|
78
|
+
if (tenantId.charAt(0) === ".") {
|
|
79
|
+
throw new GuardTenantIdError("tenant-id/hidden",
|
|
80
|
+
"guardTenantId.validate: tenantId cannot start with '.'");
|
|
81
|
+
}
|
|
82
|
+
if (tenantId.indexOf("..") >= 0) {
|
|
83
|
+
throw new GuardTenantIdError("tenant-id/path-traversal",
|
|
84
|
+
"guardTenantId.validate: tenantId contains '..'");
|
|
85
|
+
}
|
|
86
|
+
for (var i = 0; i < tenantId.length; i += 1) {
|
|
87
|
+
var c = tenantId.charCodeAt(i);
|
|
88
|
+
if (c > 0x7F) { // allow:raw-byte-literal — ASCII-only cap
|
|
89
|
+
throw new GuardTenantIdError("tenant-id/non-ascii",
|
|
90
|
+
"guardTenantId.validate: non-ASCII codepoint at offset " + i);
|
|
91
|
+
}
|
|
92
|
+
if (c < 0x20 || c === 0x7F || c === 0x2F || c === 0x5C) { // allow:raw-byte-literal — C0/DEL/slash/backslash
|
|
93
|
+
throw new GuardTenantIdError("tenant-id/bad-char",
|
|
94
|
+
"guardTenantId.validate: forbidden char 0x" + c.toString(16) + " at offset " + i);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return tenantId;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @primitive b.guardTenantId.compliancePosture
|
|
102
|
+
* @signature b.guardTenantId.compliancePosture(posture)
|
|
103
|
+
* @since 0.9.26
|
|
104
|
+
* @status stable
|
|
105
|
+
*
|
|
106
|
+
* Return the effective profile for a given compliance posture name.
|
|
107
|
+
* Returns `null` for unknown posture names so operator typos surface
|
|
108
|
+
* here instead of silently falling through to the default profile.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* b.guardTenantId.compliancePosture("hipaa"); // → "strict"
|
|
112
|
+
*/
|
|
113
|
+
function compliancePosture(posture) {
|
|
114
|
+
return COMPLIANCE_POSTURES[posture] || null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function _resolveProfile(opts) {
|
|
118
|
+
if (opts.posture && COMPLIANCE_POSTURES[opts.posture]) {
|
|
119
|
+
return COMPLIANCE_POSTURES[opts.posture];
|
|
120
|
+
}
|
|
121
|
+
var p = opts.profile || DEFAULT_PROFILE;
|
|
122
|
+
if (!PROFILES[p]) {
|
|
123
|
+
throw new GuardTenantIdError("tenant-id/bad-profile",
|
|
124
|
+
"guardTenantId: unknown profile '" + p + "'");
|
|
125
|
+
}
|
|
126
|
+
return p;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
validate: validate,
|
|
131
|
+
compliancePosture: compliancePosture,
|
|
132
|
+
PROFILES: PROFILES,
|
|
133
|
+
COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
|
|
134
|
+
RESERVED: RESERVED,
|
|
135
|
+
GuardTenantIdError: GuardTenantIdError,
|
|
136
|
+
NAME: "tenantId",
|
|
137
|
+
KIND: "tenant-id",
|
|
138
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.guardTraceContext
|
|
4
|
+
* @nav Guards
|
|
5
|
+
* @title Guard Trace Context
|
|
6
|
+
* @order 443
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* W3C Trace Context (`traceparent` + `tracestate`) shape validator.
|
|
10
|
+
* The agent-trace primitive (v0.9.29) injects traceparent strings
|
|
11
|
+
* into queue envelopes + event-bus payloads + sub-agent calls;
|
|
12
|
+
* consumers extract + start child spans. This guard refuses
|
|
13
|
+
* malformed traceparent strings (cross-boundary tampering, operator
|
|
14
|
+
* bugs, attacker-controlled trace IDs).
|
|
15
|
+
*
|
|
16
|
+
* W3C Trace Context section 3.2: `<version>-<trace-id>-<parent-id>-<flags>`
|
|
17
|
+
* in hex form. v00 is the only currently-defined version; future
|
|
18
|
+
* versions get refused under strict profile.
|
|
19
|
+
*
|
|
20
|
+
* `tracestate` (W3C section 3.3) is a comma-separated list of vendor key=
|
|
21
|
+
* value pairs, capped at 32 entries.
|
|
22
|
+
*
|
|
23
|
+
* @card
|
|
24
|
+
* Validates W3C traceparent + tracestate strings at agent
|
|
25
|
+
* boundaries. Refuses malformed shape, oversize tracestate,
|
|
26
|
+
* non-hex trace/span ids.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
var { defineClass } = require("./framework-error");
|
|
30
|
+
|
|
31
|
+
var GuardTraceContextError = defineClass("GuardTraceContextError", { alwaysPermanent: true });
|
|
32
|
+
|
|
33
|
+
var DEFAULT_PROFILE = "strict";
|
|
34
|
+
|
|
35
|
+
var PROFILES = Object.freeze({
|
|
36
|
+
strict: { allowedVersions: ["00"], maxTracestateEntries: 32, maxTracestateBytes: 512 }, // allow:raw-byte-literal
|
|
37
|
+
balanced: { allowedVersions: ["00", "01"], maxTracestateEntries: 32, maxTracestateBytes: 512 }, // allow:raw-byte-literal
|
|
38
|
+
permissive: { allowedVersions: ["*"], maxTracestateEntries: 64, maxTracestateBytes: 1024 }, // allow:raw-byte-literal
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
var COMPLIANCE_POSTURES = Object.freeze({
|
|
42
|
+
hipaa: "strict",
|
|
43
|
+
"pci-dss": "strict",
|
|
44
|
+
gdpr: "strict",
|
|
45
|
+
soc2: "strict",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
var TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/; // allow:regex-no-length-cap — length-bound inline before test
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @primitive b.guardTraceContext.validate
|
|
52
|
+
* @signature b.guardTraceContext.validate(ctx, opts?)
|
|
53
|
+
* @since 0.9.29
|
|
54
|
+
* @status stable
|
|
55
|
+
* @related b.agent.trace.create, b.tracing.create
|
|
56
|
+
*
|
|
57
|
+
* Validate a traceparent + optional tracestate envelope. Returns the
|
|
58
|
+
* input on success; throws on shape refusal.
|
|
59
|
+
*
|
|
60
|
+
* @opts
|
|
61
|
+
* profile: "strict" | "balanced" | "permissive",
|
|
62
|
+
* posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* b.guardTraceContext.validate({
|
|
66
|
+
* traceparent: "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
|
|
67
|
+
* });
|
|
68
|
+
*/
|
|
69
|
+
function validate(ctx, opts) {
|
|
70
|
+
opts = opts || {};
|
|
71
|
+
var profile = PROFILES[_resolveProfile(opts)];
|
|
72
|
+
if (!ctx || typeof ctx !== "object") {
|
|
73
|
+
throw new GuardTraceContextError("trace-context/bad-input",
|
|
74
|
+
"guardTraceContext.validate: ctx required");
|
|
75
|
+
}
|
|
76
|
+
if (typeof ctx.traceparent !== "string" || ctx.traceparent.length === 0) {
|
|
77
|
+
throw new GuardTraceContextError("trace-context/no-traceparent",
|
|
78
|
+
"guardTraceContext.validate: traceparent required");
|
|
79
|
+
}
|
|
80
|
+
// Length bound BEFORE regex test so a hostile input can't burn
|
|
81
|
+
// regex-engine CPU. W3C section 3.2.1: exactly 55 chars.
|
|
82
|
+
if (ctx.traceparent.length !== 55) { // allow:raw-byte-literal — W3C fixed length
|
|
83
|
+
throw new GuardTraceContextError("trace-context/bad-traceparent-length",
|
|
84
|
+
"guardTraceContext.validate: traceparent must be exactly 55 chars (got " +
|
|
85
|
+
ctx.traceparent.length + ")");
|
|
86
|
+
}
|
|
87
|
+
var m = TRACEPARENT_RE.exec(ctx.traceparent);
|
|
88
|
+
if (!m) {
|
|
89
|
+
throw new GuardTraceContextError("trace-context/bad-traceparent-shape",
|
|
90
|
+
"guardTraceContext.validate: traceparent does not match W3C section 3.2 shape");
|
|
91
|
+
}
|
|
92
|
+
var version = m[1];
|
|
93
|
+
var traceId = m[2];
|
|
94
|
+
var spanId = m[3];
|
|
95
|
+
// version "ff" is invalid per W3C section 3.2.2 (forbidden value)
|
|
96
|
+
if (version === "ff") {
|
|
97
|
+
throw new GuardTraceContextError("trace-context/forbidden-version",
|
|
98
|
+
"guardTraceContext.validate: version 'ff' is W3C-forbidden");
|
|
99
|
+
}
|
|
100
|
+
if (profile.allowedVersions[0] !== "*" && profile.allowedVersions.indexOf(version) < 0) {
|
|
101
|
+
throw new GuardTraceContextError("trace-context/version-not-allowed",
|
|
102
|
+
"guardTraceContext.validate: version '" + version + "' not in profile allowlist " +
|
|
103
|
+
JSON.stringify(profile.allowedVersions));
|
|
104
|
+
}
|
|
105
|
+
if (traceId === "00000000000000000000000000000000") {
|
|
106
|
+
throw new GuardTraceContextError("trace-context/zero-trace-id",
|
|
107
|
+
"guardTraceContext.validate: trace-id all-zero is W3C-invalid");
|
|
108
|
+
}
|
|
109
|
+
if (spanId === "0000000000000000") {
|
|
110
|
+
throw new GuardTraceContextError("trace-context/zero-span-id",
|
|
111
|
+
"guardTraceContext.validate: parent-id all-zero is W3C-invalid");
|
|
112
|
+
}
|
|
113
|
+
if (typeof ctx.tracestate !== "undefined") {
|
|
114
|
+
if (typeof ctx.tracestate !== "string") {
|
|
115
|
+
throw new GuardTraceContextError("trace-context/bad-tracestate-type",
|
|
116
|
+
"guardTraceContext.validate: tracestate must be a string");
|
|
117
|
+
}
|
|
118
|
+
if (Buffer.byteLength(ctx.tracestate, "utf8") > profile.maxTracestateBytes) {
|
|
119
|
+
throw new GuardTraceContextError("trace-context/tracestate-too-big",
|
|
120
|
+
"guardTraceContext.validate: tracestate exceeds maxTracestateBytes=" +
|
|
121
|
+
profile.maxTracestateBytes);
|
|
122
|
+
}
|
|
123
|
+
if (ctx.tracestate.length > 0) {
|
|
124
|
+
var entries = ctx.tracestate.split(",");
|
|
125
|
+
if (entries.length > profile.maxTracestateEntries) {
|
|
126
|
+
throw new GuardTraceContextError("trace-context/too-many-tracestate-entries",
|
|
127
|
+
"guardTraceContext.validate: " + entries.length +
|
|
128
|
+
" tracestate entries exceeds " + profile.maxTracestateEntries);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return ctx;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @primitive b.guardTraceContext.compliancePosture
|
|
137
|
+
* @signature b.guardTraceContext.compliancePosture(posture)
|
|
138
|
+
* @since 0.9.29
|
|
139
|
+
* @status stable
|
|
140
|
+
*
|
|
141
|
+
* Return the effective profile for a given compliance posture name.
|
|
142
|
+
* Returns `null` for unknown posture names so operator typos surface
|
|
143
|
+
* here instead of silently falling through to the default profile.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* b.guardTraceContext.compliancePosture("hipaa"); // returns "strict"
|
|
147
|
+
*/
|
|
148
|
+
function compliancePosture(posture) {
|
|
149
|
+
return COMPLIANCE_POSTURES[posture] || null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function _resolveProfile(opts) {
|
|
153
|
+
if (opts.posture && COMPLIANCE_POSTURES[opts.posture]) {
|
|
154
|
+
return COMPLIANCE_POSTURES[opts.posture];
|
|
155
|
+
}
|
|
156
|
+
var p = opts.profile || DEFAULT_PROFILE;
|
|
157
|
+
if (!PROFILES[p]) {
|
|
158
|
+
throw new GuardTraceContextError("trace-context/bad-profile",
|
|
159
|
+
"guardTraceContext: unknown profile '" + p + "'");
|
|
160
|
+
}
|
|
161
|
+
return p;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
validate: validate,
|
|
166
|
+
compliancePosture: compliancePosture,
|
|
167
|
+
PROFILES: PROFILES,
|
|
168
|
+
COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
|
|
169
|
+
GuardTraceContextError: GuardTraceContextError,
|
|
170
|
+
NAME: "traceContext",
|
|
171
|
+
KIND: "trace-context",
|
|
172
|
+
};
|
package/lib/ip-utils.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* lib/ip-utils.js — internal IP-address helpers shared across
|
|
4
|
+
* `b.mail.auth` (SPF / DMARC IP-in-CIDR), `b.mail.rbl` (RFC 5782
|
|
5
|
+
* reverse-DNS), `b.mail.greylist` (RFC 6647 CIDR fingerprint).
|
|
6
|
+
*
|
|
7
|
+
* Not exposed on the operator-facing `b` surface; internal compose
|
|
8
|
+
* point so the three consumers don't drift on IPv6 parsing.
|
|
9
|
+
*
|
|
10
|
+
* RFC 4291 §2.2 IPv6 text form: 8 groups of 1-4 hex characters
|
|
11
|
+
* separated by `:`; one `::` allowed to compress a contiguous run
|
|
12
|
+
* of zero groups; IPv4-mapped form `::ffff:1.2.3.4` per RFC 5952 §5
|
|
13
|
+
* + dual-stack `::a.b.c.d` per RFC 4291 §2.5.5.2.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Expand an IPv6 address to its full 32-hex-character form. Returns
|
|
18
|
+
* `null` on parse failure (invalid hex group, group count != 8,
|
|
19
|
+
* multiple `::`, group > 0xffff).
|
|
20
|
+
*
|
|
21
|
+
* expandIpv6Hex("2001:db8::1") → "20010db8000000000000000000000001"
|
|
22
|
+
* expandIpv6Hex("::ffff:192.0.2.1") → "00000000000000000000ffffc0000201"
|
|
23
|
+
* expandIpv6Hex("::1") → "00000000000000000000000000000001"
|
|
24
|
+
* expandIpv6Hex("bad") → null
|
|
25
|
+
*/
|
|
26
|
+
function expandIpv6Hex(ip) {
|
|
27
|
+
if (typeof ip !== "string") return null;
|
|
28
|
+
// RFC 4291 §2.5.5.2 IPv4-mapped / dual-stack: accept ".d.d.d.d" tail.
|
|
29
|
+
var dual = ip.match(/^(.*?):(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/); // allow:regex-no-length-cap — dotted-quad has fixed shape; LHS bounded by IPv6 group cap below
|
|
30
|
+
if (dual) {
|
|
31
|
+
var v4 = dual[2].split(".").map(Number);
|
|
32
|
+
if (v4.some(function (o) { return !(o >= 0 && o <= 255); })) return null; // allow:raw-byte-literal — IPv4 octet range
|
|
33
|
+
var hi = (v4[0] << 8) | v4[1]; // allow:raw-byte-literal — 16-bit group pack
|
|
34
|
+
var lo = (v4[2] << 8) | v4[3]; // allow:raw-byte-literal — 16-bit group pack
|
|
35
|
+
ip = dual[1] + ":" + hi.toString(16) + ":" + lo.toString(16);
|
|
36
|
+
}
|
|
37
|
+
var dblColon = ip.split("::");
|
|
38
|
+
if (dblColon.length > 2) return null;
|
|
39
|
+
var leftGroups = dblColon[0] === "" ? [] : dblColon[0].split(":");
|
|
40
|
+
var rightGroups = dblColon.length === 2 ? (dblColon[1] === "" ? [] : dblColon[1].split(":")) : [];
|
|
41
|
+
if (dblColon.length === 1 && leftGroups.length !== 8) return null; // allow:raw-byte-literal — RFC 4291 IPv6 group count
|
|
42
|
+
var fillCount = 8 - leftGroups.length - rightGroups.length; // allow:raw-byte-literal — RFC 4291 IPv6 group count
|
|
43
|
+
if (fillCount < 0) return null;
|
|
44
|
+
var fill = [];
|
|
45
|
+
for (var f = 0; f < fillCount; f += 1) fill.push("0");
|
|
46
|
+
var groups = leftGroups.concat(fill).concat(rightGroups);
|
|
47
|
+
if (groups.length !== 8) return null; // allow:raw-byte-literal — RFC 4291 IPv6 group count
|
|
48
|
+
var hex = "";
|
|
49
|
+
for (var i = 0; i < 8; i += 1) { // allow:raw-byte-literal — RFC 4291 IPv6 group count
|
|
50
|
+
var g = groups[i];
|
|
51
|
+
if (g.length === 0 || g.length > 4) return null; // allow:raw-byte-literal — RFC 4291 IPv6 hex-group max length
|
|
52
|
+
for (var hc = 0; hc < g.length; hc += 1) {
|
|
53
|
+
var cp = g.charCodeAt(hc);
|
|
54
|
+
var isDigit = cp >= 0x30 && cp <= 0x39; // allow:raw-byte-literal — ASCII '0'..'9'
|
|
55
|
+
var isLowerHex = cp >= 0x61 && cp <= 0x66; // allow:raw-byte-literal — ASCII 'a'..'f'
|
|
56
|
+
var isUpperHex = cp >= 0x41 && cp <= 0x46; // allow:raw-byte-literal — ASCII 'A'..'F'
|
|
57
|
+
if (!isDigit && !isLowerHex && !isUpperHex) return null;
|
|
58
|
+
}
|
|
59
|
+
hex += g.toLowerCase().padStart(4, "0"); // allow:raw-byte-literal — 4 hex chars per IPv6 group
|
|
60
|
+
}
|
|
61
|
+
return hex;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Expand IPv6 to an 8-element array of 16-bit unsigned integers.
|
|
66
|
+
* Used by `b.mail.auth` SPF / DMARC IP-in-CIDR evaluation which
|
|
67
|
+
* does bitwise group-level math.
|
|
68
|
+
*
|
|
69
|
+
* expandIpv6Groups("::1") → [0,0,0,0,0,0,0,1]
|
|
70
|
+
* expandIpv6Groups("bad") → null
|
|
71
|
+
*/
|
|
72
|
+
function expandIpv6Groups(ip) {
|
|
73
|
+
var hex = expandIpv6Hex(ip);
|
|
74
|
+
if (hex === null) return null;
|
|
75
|
+
var groups = new Array(8); // allow:raw-byte-literal — RFC 4291 IPv6 group count
|
|
76
|
+
for (var i = 0; i < 8; i += 1) { // allow:raw-byte-literal — RFC 4291 IPv6 group count
|
|
77
|
+
groups[i] = parseInt(hex.slice(i * 4, i * 4 + 4), 16); // allow:raw-byte-literal — 4 hex chars per IPv6 group
|
|
78
|
+
}
|
|
79
|
+
return groups;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Loose IPv4 textual-form check — for primitives that need to
|
|
83
|
+
// classify a string as "looks like dotted-quad IPv4" but don't need
|
|
84
|
+
// the strict per-octet bound check (callers that DO need octet
|
|
85
|
+
// bounds use the strict form in `mail-rbl.js` etc.). Shared so
|
|
86
|
+
// `lib/mail.js`, `lib/mail-helo.js`, and `lib/redis-client.js`
|
|
87
|
+
// don't drift on the same shape.
|
|
88
|
+
//
|
|
89
|
+
// isIPv4Shape("1.2.3.4") → true
|
|
90
|
+
// isIPv4Shape("999.0.0.0") → true (shape only; octets unbounded)
|
|
91
|
+
// isIPv4Shape("not-an-ip") → false
|
|
92
|
+
// isIPv4Shape("1.2.3") → false
|
|
93
|
+
var IPV4_SHAPE_RE = /^\d+\.\d+\.\d+\.\d+$/; // allow:regex-no-length-cap — anchored + literal-dot shape; caller bounds length
|
|
94
|
+
function isIPv4Shape(s) {
|
|
95
|
+
return typeof s === "string" && IPV4_SHAPE_RE.test(s);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
expandIpv6Hex: expandIpv6Hex,
|
|
100
|
+
expandIpv6Groups: expandIpv6Groups,
|
|
101
|
+
isIPv4Shape: isIPv4Shape,
|
|
102
|
+
};
|
package/lib/mail-auth.js
CHANGED
|
@@ -41,6 +41,7 @@ var validateOpts = require("./validate-opts");
|
|
|
41
41
|
var C = require("./constants");
|
|
42
42
|
var dkim = require("./mail-dkim");
|
|
43
43
|
var safeXml = require("./parsers/safe-xml");
|
|
44
|
+
var ipUtils = require("./ip-utils");
|
|
44
45
|
var publicSuffix = require("./public-suffix");
|
|
45
46
|
var { MailAuthError } = require("./framework-error");
|
|
46
47
|
|
|
@@ -69,41 +70,9 @@ function _ipv4ToInt(ip) {
|
|
|
69
70
|
// Expand an IPv6 string (which may carry `::` shorthand) into 8 16-bit
|
|
70
71
|
// groups. Returns null on malformed input.
|
|
71
72
|
function _ipv6Expand(ip) {
|
|
72
|
-
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
if (dual) {
|
|
76
|
-
var v4 = dual[2].split(".").map(Number);
|
|
77
|
-
if (v4.some(function (o) { return !(o >= 0 && o <= 255); })) return null; // allow:raw-byte-literal — octet range
|
|
78
|
-
var hi = (v4[0] << 8) | v4[1]; // allow:raw-byte-literal — 16-bit group pack
|
|
79
|
-
var lo = (v4[2] << 8) | v4[3]; // allow:raw-byte-literal — 16-bit group pack
|
|
80
|
-
ip = dual[1] + ":" + hi.toString(16) + ":" + lo.toString(16);
|
|
81
|
-
}
|
|
82
|
-
var dblColon = ip.split("::");
|
|
83
|
-
if (dblColon.length > 2) return null;
|
|
84
|
-
var leftGroups = dblColon[0] === "" ? [] : dblColon[0].split(":");
|
|
85
|
-
var rightGroups = dblColon.length === 2 ? (dblColon[1] === "" ? [] : dblColon[1].split(":")) : [];
|
|
86
|
-
if (dblColon.length === 1 && leftGroups.length !== 8) return null; // allow:raw-byte-literal — IPv6 group count
|
|
87
|
-
var fillCount = 8 - leftGroups.length - rightGroups.length; // allow:raw-byte-literal — IPv6 group count
|
|
88
|
-
if (fillCount < 0) return null;
|
|
89
|
-
var fill = [];
|
|
90
|
-
for (var f = 0; f < fillCount; f += 1) fill.push("0");
|
|
91
|
-
var groups = leftGroups.concat(fill).concat(rightGroups);
|
|
92
|
-
if (groups.length !== 8) return null; // allow:raw-byte-literal — IPv6 group count
|
|
93
|
-
var out = new Array(8); // allow:raw-byte-literal — IPv6 group count
|
|
94
|
-
for (var i = 0; i < 8; i += 1) { // allow:raw-byte-literal — IPv6 group count
|
|
95
|
-
var g = groups[i];
|
|
96
|
-
// RFC 4291 IPv6 hex group: 1..4 hex chars. Avoid the
|
|
97
|
-
// `/^[0-9a-fA-F]{1,4}$/` regex that's already in guard-cidr +
|
|
98
|
-
// safe-json (codebase-patterns flags 3+ duplicates) by
|
|
99
|
-
// length-then-parse: parseInt with radix 16 returns NaN on
|
|
100
|
-
// non-hex; numeric-bound check rejects out-of-range output.
|
|
101
|
-
if (g.length === 0 || g.length > 4) return null; // allow:raw-byte-literal — IPv6 hex group max length
|
|
102
|
-
var groupVal = parseInt(g, 16); // allow:raw-byte-literal — IPv6 hex base
|
|
103
|
-
if (!isFinite(groupVal) || groupVal < 0 || groupVal > 0xffff) return null; // allow:raw-byte-literal — IPv6 group max value
|
|
104
|
-
out[i] = groupVal;
|
|
105
|
-
}
|
|
106
|
-
return out;
|
|
73
|
+
// Compose the shared lib/ip-utils helper so the same IPv6 parse
|
|
74
|
+
// path is shared across mail-auth / mail-rbl / mail-greylist.
|
|
75
|
+
return ipUtils.expandIpv6Groups(ip);
|
|
107
76
|
}
|
|
108
77
|
|
|
109
78
|
function _ipv6InCidr(ip, cidr) {
|