@blamejs/core 0.9.28 → 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.
@@ -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
+ };
@@ -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
- if (typeof ip !== "string") return null;
73
- // Accept IPv4-in-IPv6 dual-stack form (e.g. ::ffff:1.2.3.4).
74
- var dual = ip.match(/^(.*?):(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
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) {