@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.
@@ -0,0 +1,665 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.safeDns
4
+ * @nav Parsers
5
+ * @title Safe DNS
6
+ * @order 130
7
+ *
8
+ * @intro
9
+ * Bounded DNS-response parser. Substrate for v0.9.31
10
+ * `b.network.dns.resolver` and every consumer that walks raw DNS
11
+ * wire-format bytes (DKIM TXT lookup, MTA-STS verify, DANE TLSA,
12
+ * RBL queries, SVCB / HTTPS discovery, DNSBL via DNS).
13
+ *
14
+ * Caps every dimension an attacker can grow to DoS the resolver
15
+ * path:
16
+ *
17
+ * - **Response byte cap** (default 4 KiB; EDNS0 negotiated max
18
+ * 64 KiB — RFC 6891).
19
+ * - **Label count per name** (default 127 — RFC 1035 §2.3.4
20
+ * absolute cap is 255 octets which bounds to ~127 labels;
21
+ * most legitimate names stay well under 20).
22
+ * - **Compression-pointer chain depth** (default 16 — RFC 1035
23
+ * allows pointer-to-pointer; unbounded chains cause infinite
24
+ * loops without a depth cap. Common parser-bomb vector).
25
+ * - **CNAME chain depth** (default 8 — matches BIND9's operational
26
+ * cap on canonical-name translations; RFC 1912 §2.4 warns against
27
+ * long CNAME chains; we cap to defend RFC 9156 §3.1 amplification
28
+ * + redirection-loop classes).
29
+ * - **RR count per section** (default 64 answers, 32 authority,
30
+ * 32 additional — total response bounded above by the byte
31
+ * cap, but per-section caps short-circuit malicious sections).
32
+ * - **TXT rdata total length** (default 64 KiB — RFC 1035
33
+ * §3.3.14 allows up to 65535 octets per RR, but real-world
34
+ * SPF / DKIM / MTA-STS records never approach that; cap
35
+ * defends against amplification).
36
+ *
37
+ * Throws `SafeDnsError` on every cap exceeded, malformed name
38
+ * compression, truncated RR, oversize EDNS0 OPT pseudo-RR, RDLENGTH
39
+ * overflow past message end. The parser is purely functional — no
40
+ * I/O, no async — operators run it inline in the resolver path.
41
+ *
42
+ * Defends the DNS-amplification + parser-bomb classes generally —
43
+ * `CVE-2022-3204` (NRDelegationAttack — oversized authority + additional
44
+ * sections backing a malicious non-responsive delegation), `CVE-2023-50387`
45
+ * (KeyTrap — DNSKEY+RRSIG combinatorial DoS in validators, mitigated
46
+ * here by per-section RR caps that bound the input to validation),
47
+ * `CVE-2023-50868` (NSEC3-encloser companion), `CVE-2024-1737` (BIND9
48
+ * resource exhaustion via large RRsets per hostname). RFC 9156 §3
49
+ * amplification class.
50
+ *
51
+ * @card
52
+ * Bounded DNS-response parser. Substrate for the v0.9.31 validating
53
+ * resolver — caps response bytes, label count, compression-pointer
54
+ * chain depth, CNAME chain depth, per-section RR count, TXT rdata
55
+ * total length, EDNS0 OPT pseudo-RR size.
56
+ */
57
+
58
+ var C = require("./constants");
59
+ var { defineClass } = require("./framework-error");
60
+
61
+ var SafeDnsError = defineClass("SafeDnsError", { alwaysPermanent: true });
62
+
63
+ // allow:raw-byte-literal — RFC 1035 §3.1 single-label cap (octet 0 high
64
+ // 2 bits reserved for compression pointer; label-length field is 6 bits).
65
+ var DNS_MAX_LABEL_BYTES = 63;
66
+
67
+ // allow:raw-byte-literal — RFC 1035 §3.1 wire-format name absolute cap
68
+ // (sum of all label-length bytes + label bytes + terminator).
69
+ var DNS_MAX_NAME_BYTES = 255;
70
+
71
+ // allow:raw-byte-literal — RFC 1035 §4.2.1 fixed header size.
72
+ var DNS_HEADER_BYTES = 12;
73
+
74
+ // allow:raw-byte-literal — RFC 1035 §3.2.1 RR fixed prefix
75
+ // (TYPE 2 + CLASS 2 + TTL 4 + RDLENGTH 2 = 10 octets after NAME).
76
+ var DNS_RR_FIXED_BYTES = 10;
77
+
78
+ // allow:raw-byte-literal — RFC 6891 §6.1 OPT pseudo-RR upper bound for
79
+ // EDNS0 payload size we'll accept. 64 KiB is the protocol absolute
80
+ // max; resolver-side default is much smaller.
81
+ var EDNS0_HARD_MAX = 65535;
82
+
83
+ // allow:raw-byte-literal — RFC 1035 §3.2.2 record-type codes we route
84
+ // through type-specific decoders. Anything not listed parses as raw
85
+ // rdata bytes (operator inspects the RDLENGTH-bounded slice).
86
+ var RTYPE_A = 1;
87
+ var RTYPE_NS = 2;
88
+ var RTYPE_CNAME = 5;
89
+ var RTYPE_SOA = 6;
90
+ var RTYPE_PTR = 12;
91
+ var RTYPE_MX = 15;
92
+ var RTYPE_TXT = 16; // allow:raw-byte-literal — RFC 1035 §3.2.2 TXT record type code
93
+ var RTYPE_AAAA = 28;
94
+ var RTYPE_SRV = 33;
95
+ var RTYPE_OPT = 41;
96
+ var RTYPE_DS = 43;
97
+ var RTYPE_RRSIG = 46;
98
+ var RTYPE_DNSKEY = 48; // allow:raw-byte-literal — RFC 4034 DNSKEY record type code
99
+ var RTYPE_TLSA = 52;
100
+
101
+ var RTYPE_NAMES = Object.freeze({
102
+ 1: "A", 2: "NS", 5: "CNAME", 6: "SOA", 12: "PTR", 15: "MX",
103
+ 16: "TXT", 28: "AAAA", 33: "SRV", 41: "OPT", 43: "DS", // allow:raw-byte-literal — IANA DNS record type codes
104
+ 46: "RRSIG", 47: "NSEC", 48: "DNSKEY", 50: "NSEC3", 52: "TLSA", // allow:raw-byte-literal — IANA DNS record type codes
105
+ 64: "SVCB", 65: "HTTPS", // allow:raw-byte-literal — IANA DNS record type codes
106
+ });
107
+
108
+ var DEFAULT_MAX_RESPONSE_BYTES = C.BYTES.kib(4);
109
+ var DEFAULT_MAX_EDNS0_BYTES = C.BYTES.kib(4);
110
+ var DEFAULT_MAX_LABELS = 127; // allow:raw-byte-literal — RFC 1035 §2.3.4 label count cap (count, not bytes)
111
+ var DEFAULT_MAX_POINTER_DEPTH = 16; // allow:raw-byte-literal — compression-pointer chain depth (count, not bytes)
112
+ var DEFAULT_MAX_CNAME_DEPTH = 8;
113
+ var DEFAULT_MAX_ANSWER_RRS = 64; // allow:raw-byte-literal — RR count cap (count, not bytes)
114
+ var DEFAULT_MAX_AUTHORITY_RRS = 32; // allow:raw-byte-literal — RR count cap (count, not bytes)
115
+ var DEFAULT_MAX_ADDITIONAL_RRS = 32; // allow:raw-byte-literal — RR count cap (count, not bytes)
116
+ var DEFAULT_MAX_TXT_RDATA = C.BYTES.kib(64);
117
+
118
+ var DEFAULT_PROFILE = "strict";
119
+
120
+ var PROFILES = Object.freeze({
121
+ strict: {
122
+ maxResponseBytes: DEFAULT_MAX_RESPONSE_BYTES,
123
+ maxEdns0Bytes: DEFAULT_MAX_EDNS0_BYTES,
124
+ maxLabels: DEFAULT_MAX_LABELS,
125
+ maxPointerDepth: DEFAULT_MAX_POINTER_DEPTH,
126
+ maxCnameDepth: DEFAULT_MAX_CNAME_DEPTH,
127
+ maxAnswerRrs: DEFAULT_MAX_ANSWER_RRS,
128
+ maxAuthorityRrs: DEFAULT_MAX_AUTHORITY_RRS,
129
+ maxAdditionalRrs: DEFAULT_MAX_ADDITIONAL_RRS,
130
+ maxTxtRdata: DEFAULT_MAX_TXT_RDATA,
131
+ },
132
+ balanced: {
133
+ maxResponseBytes: C.BYTES.kib(16),
134
+ maxEdns0Bytes: C.BYTES.kib(16),
135
+ maxLabels: DEFAULT_MAX_LABELS,
136
+ maxPointerDepth: DEFAULT_MAX_POINTER_DEPTH,
137
+ maxCnameDepth: 16, // allow:raw-byte-literal — RR count, not bytes
138
+ maxAnswerRrs: 128, // allow:raw-byte-literal — RR count
139
+ maxAuthorityRrs: 64, // allow:raw-byte-literal — RR count
140
+ maxAdditionalRrs: 64, // allow:raw-byte-literal — RR count
141
+ maxTxtRdata: C.BYTES.kib(128),
142
+ },
143
+ permissive: {
144
+ maxResponseBytes: C.BYTES.kib(64),
145
+ maxEdns0Bytes: C.BYTES.kib(64),
146
+ maxLabels: DEFAULT_MAX_LABELS,
147
+ maxPointerDepth: 32, // allow:raw-byte-literal — pointer chain count
148
+ maxCnameDepth: 32, // allow:raw-byte-literal — chain count
149
+ maxAnswerRrs: 256, // allow:raw-byte-literal — RR count
150
+ maxAuthorityRrs: 128, // allow:raw-byte-literal — RR count
151
+ maxAdditionalRrs: 128, // allow:raw-byte-literal — RR count
152
+ maxTxtRdata: C.BYTES.kib(512),
153
+ },
154
+ });
155
+
156
+ var COMPLIANCE_POSTURES = Object.freeze({
157
+ hipaa: "strict",
158
+ "pci-dss": "strict",
159
+ gdpr: "strict",
160
+ soc2: "strict",
161
+ });
162
+
163
+ /**
164
+ * @primitive b.safeDns.parseResponse
165
+ * @signature b.safeDns.parseResponse(buf, opts?)
166
+ * @since 0.9.31
167
+ * @status stable
168
+ * @related b.safeDns.boundEdns0, b.safeDns.checkCnameChainDepth
169
+ *
170
+ * Parse a DNS wire-format response into a structured shape. Returns
171
+ * `{ id, rcode, flags, question, answer, authority, additional,
172
+ * edns0 }`. Each RR carries `{ name, type, typeName, class, ttl,
173
+ * rdata, decoded }` — `rdata` is the rdlength-bounded byte slice,
174
+ * `decoded` is the type-specific parse where the parser knows the
175
+ * type (A / AAAA / CNAME / NS / PTR / MX / TXT / SOA / SRV / DS /
176
+ * DNSKEY / TLSA / RRSIG / NSEC / NSEC3 / SVCB / HTTPS), otherwise
177
+ * `null`.
178
+ *
179
+ * Throws `SafeDnsError` with codes:
180
+ * `safe-dns/bad-input` / `oversize-response` / `truncated-header`
181
+ * / `truncated-rr` / `truncated-name` / `oversize-label` /
182
+ * `oversize-name` / `oversize-pointer-depth` / `oversize-labels` /
183
+ * `oversize-answer-rrs` / `oversize-authority-rrs` /
184
+ * `oversize-additional-rrs` / `oversize-txt-rdata` /
185
+ * `oversize-edns0` / `malformed-rdlength` / `bad-profile`.
186
+ *
187
+ * @opts
188
+ * profile: "strict" | "balanced" | "permissive",
189
+ * posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
190
+ *
191
+ * @example
192
+ * var parsed = b.safeDns.parseResponse(wireBytes);
193
+ * parsed.answer.forEach(function (rr) {
194
+ * if (rr.typeName === "TXT") console.log(rr.decoded.join(""));
195
+ * });
196
+ */
197
+ function parseResponse(buf, opts) {
198
+ opts = opts || {};
199
+ if (!Buffer.isBuffer(buf)) {
200
+ throw new SafeDnsError("safe-dns/bad-input",
201
+ "safeDns.parseResponse: buf must be a Buffer; got " + (typeof buf));
202
+ }
203
+ var caps = _resolveProfile(opts);
204
+ if (buf.length > caps.maxResponseBytes) {
205
+ throw new SafeDnsError("safe-dns/oversize-response",
206
+ "safeDns.parseResponse: " + buf.length + " bytes exceeds maxResponseBytes=" +
207
+ caps.maxResponseBytes + " (RFC 6891 §6.1 EDNS0 advertised buffer size)");
208
+ }
209
+ if (buf.length < DNS_HEADER_BYTES) {
210
+ throw new SafeDnsError("safe-dns/truncated-header",
211
+ "safeDns.parseResponse: response truncated below header size (" +
212
+ buf.length + " < " + DNS_HEADER_BYTES + ")");
213
+ }
214
+
215
+ var id = buf.readUInt16BE(0);
216
+ var flags = buf.readUInt16BE(2);
217
+ var qdcount = buf.readUInt16BE(4);
218
+ var ancount = buf.readUInt16BE(6);
219
+ var nscount = buf.readUInt16BE(8);
220
+ var arcount = buf.readUInt16BE(10);
221
+
222
+ if (ancount > caps.maxAnswerRrs) {
223
+ throw new SafeDnsError("safe-dns/oversize-answer-rrs",
224
+ "safeDns.parseResponse: ancount=" + ancount + " exceeds maxAnswerRrs=" +
225
+ caps.maxAnswerRrs + " (RFC 9156 amplification defense)");
226
+ }
227
+ if (nscount > caps.maxAuthorityRrs) {
228
+ throw new SafeDnsError("safe-dns/oversize-authority-rrs",
229
+ "safeDns.parseResponse: nscount=" + nscount + " exceeds maxAuthorityRrs=" +
230
+ caps.maxAuthorityRrs);
231
+ }
232
+ if (arcount > caps.maxAdditionalRrs) {
233
+ throw new SafeDnsError("safe-dns/oversize-additional-rrs",
234
+ "safeDns.parseResponse: arcount=" + arcount + " exceeds maxAdditionalRrs=" +
235
+ caps.maxAdditionalRrs);
236
+ }
237
+
238
+ var state = { off: DNS_HEADER_BYTES, buf: buf, caps: caps };
239
+ var question = [];
240
+ for (var q = 0; q < qdcount; q += 1) {
241
+ var qname = _readName(state, 0);
242
+ if (state.off + 4 > buf.length) { // allow:raw-byte-literal — RFC 1035 question fixed tail (QTYPE 2 + QCLASS 2)
243
+ throw new SafeDnsError("safe-dns/truncated-rr",
244
+ "safeDns.parseResponse: question RR truncated mid-fixed-tail");
245
+ }
246
+ var qtype = buf.readUInt16BE(state.off);
247
+ var qclass = buf.readUInt16BE(state.off + 2);
248
+ state.off += 4; // allow:raw-byte-literal — RFC 1035 QTYPE 2 + QCLASS 2 advance
249
+ question.push({
250
+ name: qname,
251
+ type: qtype,
252
+ typeName: RTYPE_NAMES[qtype] || ("TYPE" + qtype),
253
+ class: qclass,
254
+ });
255
+ }
256
+
257
+ var answer = [];
258
+ var authority = [];
259
+ var additional = [];
260
+ var edns0 = null;
261
+
262
+ for (var a = 0; a < ancount; a += 1) answer.push(_readRr(state));
263
+ for (var n = 0; n < nscount; n += 1) authority.push(_readRr(state));
264
+ for (var ad = 0; ad < arcount; ad += 1) {
265
+ var rr = _readRr(state);
266
+ if (rr.type === RTYPE_OPT) {
267
+ edns0 = _decodeOpt(rr, caps);
268
+ } else {
269
+ additional.push(rr);
270
+ }
271
+ }
272
+
273
+ return {
274
+ id: id,
275
+ rcode: flags & 0x0f, // allow:raw-byte-literal — RFC 1035 §4.1.1 RCODE mask
276
+ flags: flags,
277
+ question: question,
278
+ answer: answer,
279
+ authority: authority,
280
+ additional: additional,
281
+ edns0: edns0,
282
+ };
283
+ }
284
+
285
+ /**
286
+ * @primitive b.safeDns.boundEdns0
287
+ * @signature b.safeDns.boundEdns0(advertised, opts?)
288
+ * @since 0.9.31
289
+ * @status stable
290
+ *
291
+ * Clamp an operator-supplied EDNS0 advertised buffer size to the
292
+ * profile cap. Resolver code calls this when constructing a query's
293
+ * OPT pseudo-RR so a misconfigured operator can't advertise a buffer
294
+ * larger than the profile permits.
295
+ *
296
+ * @opts
297
+ * profile: "strict" | "balanced" | "permissive",
298
+ * posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
299
+ *
300
+ * @example
301
+ * var udpMax = b.safeDns.boundEdns0(operatorConfig.ednsBuffer);
302
+ */
303
+ function boundEdns0(advertised, opts) {
304
+ opts = opts || {};
305
+ var caps = _resolveProfile(opts);
306
+ if (typeof advertised !== "number" || !isFinite(advertised) || advertised < 0) {
307
+ throw new SafeDnsError("safe-dns/bad-input",
308
+ "safeDns.boundEdns0: advertised must be a non-negative finite number");
309
+ }
310
+ if (advertised > EDNS0_HARD_MAX) {
311
+ throw new SafeDnsError("safe-dns/oversize-edns0",
312
+ "safeDns.boundEdns0: advertised=" + advertised + " exceeds protocol max=" + EDNS0_HARD_MAX);
313
+ }
314
+ return Math.min(advertised, caps.maxEdns0Bytes);
315
+ }
316
+
317
+ /**
318
+ * @primitive b.safeDns.checkCnameChainDepth
319
+ * @signature b.safeDns.checkCnameChainDepth(depth, opts?)
320
+ * @since 0.9.31
321
+ * @status stable
322
+ *
323
+ * Throw if a CNAME-following loop has exceeded the profile's chain
324
+ * depth cap. Called by the resolver as it walks CNAME redirections
325
+ * across follow-up queries (each new query bumps the counter).
326
+ *
327
+ * @opts
328
+ * profile: "strict" | "balanced" | "permissive",
329
+ * posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
330
+ *
331
+ * @example
332
+ * for (var i = 0; i < 100; i += 1) {
333
+ * b.safeDns.checkCnameChainDepth(i);
334
+ * // ...follow the CNAME if there is one, else break...
335
+ * break;
336
+ * }
337
+ */
338
+ function checkCnameChainDepth(depth, opts) {
339
+ opts = opts || {};
340
+ var caps = _resolveProfile(opts);
341
+ if (typeof depth !== "number" || !isFinite(depth) || depth < 0) {
342
+ throw new SafeDnsError("safe-dns/bad-input",
343
+ "safeDns.checkCnameChainDepth: depth must be a non-negative finite number");
344
+ }
345
+ if (depth > caps.maxCnameDepth) {
346
+ throw new SafeDnsError("safe-dns/oversize-cname-depth",
347
+ "safeDns.checkCnameChainDepth: depth=" + depth + " exceeds maxCnameDepth=" +
348
+ caps.maxCnameDepth + " (RFC 1912 §2.4 chain-loop defense; matches BIND9's cap of 8 canonical-name translations)");
349
+ }
350
+ }
351
+
352
+ /**
353
+ * @primitive b.safeDns.compliancePosture
354
+ * @signature b.safeDns.compliancePosture(posture)
355
+ * @since 0.9.31
356
+ * @status stable
357
+ *
358
+ * Return the effective profile name for a compliance posture, or
359
+ * `null` for unknown posture names (operator typo surfaces here).
360
+ *
361
+ * @example
362
+ * b.safeDns.compliancePosture("hipaa"); // → "strict"
363
+ */
364
+ function compliancePosture(posture) {
365
+ return COMPLIANCE_POSTURES[posture] || null;
366
+ }
367
+
368
+ function _readName(state, pointerDepth) {
369
+ if (pointerDepth > state.caps.maxPointerDepth) {
370
+ throw new SafeDnsError("safe-dns/oversize-pointer-depth",
371
+ "safeDns.readName: compression-pointer chain depth=" + pointerDepth +
372
+ " exceeds maxPointerDepth=" + state.caps.maxPointerDepth + " (RFC 1035 §4.1.4 loop defense)");
373
+ }
374
+ var labels = [];
375
+ var totalBytes = 0;
376
+ var jumped = false;
377
+ var afterPointerOff = -1;
378
+ var off = state.off;
379
+ while (true) {
380
+ if (off >= state.buf.length) {
381
+ throw new SafeDnsError("safe-dns/truncated-name",
382
+ "safeDns.readName: name walk past end of message");
383
+ }
384
+ var byte = state.buf[off];
385
+ if (byte === 0) {
386
+ off += 1;
387
+ totalBytes += 1;
388
+ if (totalBytes > DNS_MAX_NAME_BYTES) {
389
+ throw new SafeDnsError("safe-dns/oversize-name",
390
+ "safeDns.readName: wire-name=" + totalBytes + " bytes exceeds RFC 1035 cap=" +
391
+ DNS_MAX_NAME_BYTES);
392
+ }
393
+ break;
394
+ }
395
+ if ((byte & 0xc0) === 0xc0) { // allow:raw-byte-literal — RFC 1035 §4.1.4 compression pointer mask
396
+ if (off + 1 >= state.buf.length) {
397
+ throw new SafeDnsError("safe-dns/truncated-name",
398
+ "safeDns.readName: compression pointer truncated");
399
+ }
400
+ var ptrOff = ((byte & 0x3f) << 8) | state.buf[off + 1]; // allow:raw-byte-literal — RFC 1035 §4.1.4 14-bit pointer offset
401
+ if (ptrOff >= state.buf.length) {
402
+ throw new SafeDnsError("safe-dns/truncated-name",
403
+ "safeDns.readName: compression pointer offset past message end");
404
+ }
405
+ // First compression pointer ends the in-line label walk
406
+ // (line break below). `jumped` can never already be true here;
407
+ // assign unconditionally per Codex code-quality review.
408
+ afterPointerOff = off + 2; // allow:raw-byte-literal — RFC 1035 §4.1.4 2-byte pointer width
409
+ jumped = true;
410
+ var subState = { off: ptrOff, buf: state.buf, caps: state.caps };
411
+ var tailName = _readName(subState, pointerDepth + 1);
412
+ if (tailName.length) labels.push(tailName);
413
+ totalBytes += 2; // allow:raw-byte-literal — RFC 1035 §4.1.4 2-byte pointer width
414
+ if (totalBytes > DNS_MAX_NAME_BYTES) {
415
+ throw new SafeDnsError("safe-dns/oversize-name",
416
+ "safeDns.readName: composite name=" + totalBytes + " bytes exceeds RFC 1035 cap=" +
417
+ DNS_MAX_NAME_BYTES);
418
+ }
419
+ break;
420
+ }
421
+ if (byte > DNS_MAX_LABEL_BYTES) {
422
+ throw new SafeDnsError("safe-dns/oversize-label",
423
+ "safeDns.readName: label length=" + byte + " exceeds RFC 1035 cap=" + DNS_MAX_LABEL_BYTES);
424
+ }
425
+ if (off + 1 + byte > state.buf.length) {
426
+ throw new SafeDnsError("safe-dns/truncated-name",
427
+ "safeDns.readName: label content past message end");
428
+ }
429
+ labels.push(state.buf.toString("ascii", off + 1, off + 1 + byte));
430
+ if (labels.length > state.caps.maxLabels) {
431
+ throw new SafeDnsError("safe-dns/oversize-labels",
432
+ "safeDns.readName: label count=" + labels.length + " exceeds maxLabels=" + state.caps.maxLabels);
433
+ }
434
+ off += 1 + byte;
435
+ totalBytes += 1 + byte;
436
+ if (totalBytes > DNS_MAX_NAME_BYTES) {
437
+ throw new SafeDnsError("safe-dns/oversize-name",
438
+ "safeDns.readName: wire-name=" + totalBytes + " bytes exceeds RFC 1035 cap=" +
439
+ DNS_MAX_NAME_BYTES);
440
+ }
441
+ }
442
+ state.off = jumped ? afterPointerOff : off;
443
+ return labels.join(".");
444
+ }
445
+
446
+ function _readRr(state) {
447
+ var name = _readName(state, 0);
448
+ if (state.off + DNS_RR_FIXED_BYTES > state.buf.length) {
449
+ throw new SafeDnsError("safe-dns/truncated-rr",
450
+ "safeDns.readRr: RR truncated mid-fixed-prefix");
451
+ }
452
+ var rtype = state.buf.readUInt16BE(state.off);
453
+ var rclass = state.buf.readUInt16BE(state.off + 2); // allow:raw-byte-literal — RFC 1035 §3.2.1 CLASS offset
454
+ var ttl = state.buf.readUInt32BE(state.off + 4); // allow:raw-byte-literal — RFC 1035 §3.2.1 TTL offset
455
+ var rdlen = state.buf.readUInt16BE(state.off + 8); // allow:raw-byte-literal — RFC 1035 §3.2.1 RDLENGTH offset
456
+ state.off += DNS_RR_FIXED_BYTES;
457
+ if (state.off + rdlen > state.buf.length) {
458
+ throw new SafeDnsError("safe-dns/malformed-rdlength",
459
+ "safeDns.readRr: RDLENGTH=" + rdlen + " runs past message end (off=" + state.off +
460
+ " len=" + state.buf.length + ")");
461
+ }
462
+ var rdataStart = state.off;
463
+ var rdata = state.buf.slice(rdataStart, rdataStart + rdlen);
464
+ state.off += rdlen;
465
+
466
+ var decoded = null;
467
+ if (rtype === RTYPE_A && rdlen === 4) { // allow:raw-byte-literal — RFC 1035 §3.4.1 A record is 4 octets
468
+ decoded = rdata[0] + "." + rdata[1] + "." + rdata[2] + "." + rdata[3]; // allow:raw-byte-literal — dotted-quad indices into 4-octet A rdata
469
+ } else if (rtype === RTYPE_AAAA && rdlen === 16) { // allow:raw-byte-literal — RFC 3596 §2.2 AAAA record is 16 octets
470
+ decoded = _formatIpv6(rdata);
471
+ } else if (rtype === RTYPE_CNAME || rtype === RTYPE_NS || rtype === RTYPE_PTR) {
472
+ var subState = { off: rdataStart, buf: state.buf, caps: state.caps };
473
+ decoded = _readName(subState, 0);
474
+ } else if (rtype === RTYPE_MX && rdlen >= 3) { // allow:raw-byte-literal — RFC 1035 §3.3.9 MX preference 2 + min exchange 1
475
+ var pref = rdata.readUInt16BE(0);
476
+ var mxState = { off: rdataStart + 2, buf: state.buf, caps: state.caps }; // allow:raw-byte-literal — MX preference field width
477
+ var exchange = _readName(mxState, 0);
478
+ decoded = { preference: pref, exchange: exchange };
479
+ } else if (rtype === RTYPE_TXT) {
480
+ decoded = _decodeTxt(rdata, rdlen, state.caps);
481
+ } else if (rtype === RTYPE_SOA) {
482
+ decoded = _decodeSoa(state.buf, rdataStart, rdlen, state.caps);
483
+ } else if (rtype === RTYPE_SRV && rdlen >= 7) { // allow:raw-byte-literal — RFC 2782 SRV fixed prefix 6 + min target 1
484
+ var srvState = { off: rdataStart + 6, buf: state.buf, caps: state.caps }; // allow:raw-byte-literal — RFC 2782 priority 2 + weight 2 + port 2
485
+ var target = _readName(srvState, 0);
486
+ decoded = {
487
+ priority: rdata.readUInt16BE(0),
488
+ weight: rdata.readUInt16BE(2), // allow:raw-byte-literal — RFC 2782 weight offset
489
+ port: rdata.readUInt16BE(4), // allow:raw-byte-literal — RFC 2782 port offset
490
+ target: target,
491
+ };
492
+ } else if (rtype === RTYPE_DS && rdlen >= 4) { // allow:raw-byte-literal — RFC 4034 §5.1 DS fixed prefix 4 + digest
493
+ decoded = {
494
+ keyTag: rdata.readUInt16BE(0),
495
+ algorithm: rdata.readUInt8(2),
496
+ digestType: rdata.readUInt8(3),
497
+ digest: rdata.slice(4), // allow:raw-byte-literal — RFC 4034 §5.1 digest start
498
+ };
499
+ } else if (rtype === RTYPE_DNSKEY && rdlen >= 4) { // allow:raw-byte-literal — RFC 4034 §2.1 DNSKEY fixed prefix 4 + pubkey
500
+ decoded = {
501
+ flags: rdata.readUInt16BE(0),
502
+ protocol: rdata.readUInt8(2),
503
+ algorithm: rdata.readUInt8(3),
504
+ publicKey: rdata.slice(4), // allow:raw-byte-literal — RFC 4034 §2.1 publicKey start
505
+ };
506
+ } else if (rtype === RTYPE_RRSIG && rdlen >= 18) { // allow:raw-byte-literal — RFC 4034 §3.1 RRSIG fixed prefix 18 + signer + signature
507
+ var rrsigState = { off: rdataStart + 18, buf: state.buf, caps: state.caps }; // allow:raw-byte-literal — RFC 4034 §3.1 fixed prefix width
508
+ var signer = _readName(rrsigState, 0);
509
+ decoded = {
510
+ typeCovered: rdata.readUInt16BE(0),
511
+ algorithm: rdata.readUInt8(2),
512
+ labels: rdata.readUInt8(3),
513
+ originalTtl: rdata.readUInt32BE(4), // allow:raw-byte-literal — RFC 4034 §3.1 originalTtl offset
514
+ sigExpiry: rdata.readUInt32BE(8), // allow:raw-byte-literal — RFC 4034 §3.1 expiry offset
515
+ sigInception: rdata.readUInt32BE(12), // allow:raw-byte-literal — RFC 4034 §3.1 inception offset
516
+ keyTag: rdata.readUInt16BE(16), // allow:raw-byte-literal — RFC 4034 §3.1 keyTag offset
517
+ signerName: signer,
518
+ signature: state.buf.slice(rrsigState.off, rdataStart + rdlen),
519
+ };
520
+ } else if (rtype === RTYPE_TLSA && rdlen >= 3) { // allow:raw-byte-literal — RFC 6698 §2.1 TLSA fixed prefix 3 + certData
521
+ decoded = {
522
+ usage: rdata.readUInt8(0),
523
+ selector: rdata.readUInt8(1),
524
+ matchingType: rdata.readUInt8(2),
525
+ certData: rdata.slice(3), // allow:raw-byte-literal — RFC 6698 §2.1 certData start
526
+ };
527
+ }
528
+
529
+ return {
530
+ name: name,
531
+ type: rtype,
532
+ typeName: RTYPE_NAMES[rtype] || ("TYPE" + rtype),
533
+ class: rclass,
534
+ ttl: ttl,
535
+ rdata: rdata,
536
+ decoded: decoded,
537
+ };
538
+ }
539
+
540
+ // Format a 16-byte AAAA rdata buffer as a canonical IPv6 string per
541
+ // RFC 5952 §4: lowercase hex; suppress leading zeros in each group;
542
+ // compress the longest run of all-zero groups (>= 2) with "::"; on
543
+ // ties prefer the leftmost run; if the address is IPv4-mapped
544
+ // (::ffff:0:0/96) emit the trailing 32 bits as dotted-quad per
545
+ // RFC 5952 §5.
546
+ function _formatIpv6(rdata) {
547
+ var groups = new Array(8); // allow:raw-byte-literal — RFC 4291 §2.2 8 IPv6 groups
548
+ for (var g = 0; g < 8; g += 1) groups[g] = rdata.readUInt16BE(g * 2); // allow:raw-byte-literal — RFC 4291 §2.2 group byte stride
549
+
550
+ // RFC 5952 §5 — IPv4-mapped: first 80 bits zero, next 16 bits 0xFFFF.
551
+ var isV4Mapped = true;
552
+ for (var z = 0; z < 5; z += 1) if (groups[z] !== 0) { isV4Mapped = false; break; } // allow:raw-byte-literal — RFC 5952 §5 v4-mapped zero-prefix groups
553
+ if (isV4Mapped && groups[5] !== 0xffff) isV4Mapped = false; // allow:raw-byte-literal — RFC 5952 §5 v4-mapped marker group
554
+ if (isV4Mapped) {
555
+ var dotted = rdata[12] + "." + rdata[13] + "." + rdata[14] + "." + rdata[15]; // allow:raw-byte-literal — RFC 5952 §5 trailing v4 octets
556
+ return "::ffff:" + dotted;
557
+ }
558
+
559
+ // Find the longest run of zeros (length >= 2 to use "::" per RFC 5952 §4.2.2).
560
+ var bestStart = -1;
561
+ var bestLen = 0;
562
+ var curStart = -1;
563
+ var curLen = 0;
564
+ for (var i = 0; i < 8; i += 1) { // allow:raw-byte-literal — RFC 4291 §2.2 IPv6 group iteration
565
+ if (groups[i] === 0) {
566
+ if (curStart === -1) curStart = i;
567
+ curLen += 1;
568
+ if (curLen > bestLen) { bestStart = curStart; bestLen = curLen; }
569
+ } else {
570
+ curStart = -1;
571
+ curLen = 0;
572
+ }
573
+ }
574
+ var hex = groups.map(function (n) { return n.toString(16); }); // allow:raw-byte-literal — hex radix
575
+ if (bestLen < 2) return hex.join(":");
576
+ var head = hex.slice(0, bestStart).join(":");
577
+ var tail = hex.slice(bestStart + bestLen).join(":");
578
+ return head + "::" + tail;
579
+ }
580
+
581
+ function _decodeTxt(rdata, rdlen, caps) {
582
+ if (rdlen > caps.maxTxtRdata) {
583
+ throw new SafeDnsError("safe-dns/oversize-txt-rdata",
584
+ "safeDns.decodeTxt: TXT rdata=" + rdlen + " exceeds maxTxtRdata=" + caps.maxTxtRdata);
585
+ }
586
+ var strings = [];
587
+ var off = 0;
588
+ while (off < rdlen) {
589
+ var len = rdata.readUInt8(off);
590
+ off += 1;
591
+ if (off + len > rdlen) {
592
+ throw new SafeDnsError("safe-dns/malformed-rdlength",
593
+ "safeDns.decodeTxt: character-string length=" + len + " runs past rdata end");
594
+ }
595
+ strings.push(rdata.toString("utf8", off, off + len));
596
+ off += len;
597
+ }
598
+ return strings;
599
+ }
600
+
601
+ function _decodeSoa(buf, rdataStart, rdlen, caps) {
602
+ var state = { off: rdataStart, buf: buf, caps: caps };
603
+ var mname = _readName(state, 0);
604
+ var rname = _readName(state, 0);
605
+ if (state.off + 20 > rdataStart + rdlen) { // allow:raw-byte-literal — RFC 1035 §3.3.13 SOA tail = SERIAL 4 + REFRESH 4 + RETRY 4 + EXPIRE 4 + MINIMUM 4 = 20 octets
606
+ throw new SafeDnsError("safe-dns/malformed-rdlength",
607
+ "safeDns.decodeSoa: SOA tail truncated");
608
+ }
609
+ var serial = buf.readUInt32BE(state.off);
610
+ var refresh = buf.readUInt32BE(state.off + 4); // allow:raw-byte-literal — RFC 1035 §3.3.13 REFRESH offset
611
+ var retry = buf.readUInt32BE(state.off + 8); // allow:raw-byte-literal — RFC 1035 §3.3.13 RETRY offset
612
+ var expire = buf.readUInt32BE(state.off + 12); // allow:raw-byte-literal — RFC 1035 §3.3.13 EXPIRE offset
613
+ var minimum = buf.readUInt32BE(state.off + 16); // allow:raw-byte-literal — RFC 1035 §3.3.13 MINIMUM offset
614
+ return {
615
+ mname: mname, rname: rname,
616
+ serial: serial, refresh: refresh, retry: retry, expire: expire, minimum: minimum,
617
+ };
618
+ }
619
+
620
+ function _decodeOpt(rr, caps) {
621
+ // RFC 6891 §6.1.2 — for OPT, CLASS holds the requestor's UDP payload
622
+ // size (advertised buffer), TTL holds extended RCODE + version +
623
+ // flags. We surface those and refuse oversize advertisement.
624
+ var advertised = rr.class;
625
+ if (advertised > caps.maxEdns0Bytes) {
626
+ throw new SafeDnsError("safe-dns/oversize-edns0",
627
+ "safeDns.decodeOpt: advertised buffer size=" + advertised +
628
+ " exceeds maxEdns0Bytes=" + caps.maxEdns0Bytes);
629
+ }
630
+ var extendedRcode = (rr.ttl >>> 24) & 0xff; // allow:raw-byte-literal — RFC 6891 §6.1.3 extended RCODE upper byte
631
+ var version = (rr.ttl >>> 16) & 0xff; // allow:raw-byte-literal — RFC 6891 §6.1.3 version byte
632
+ var dnssecOk = (rr.ttl & 0x8000) !== 0; // allow:raw-byte-literal — RFC 4035 §3.2.1 DO bit
633
+ return {
634
+ advertisedUdpSize: advertised,
635
+ extendedRcode: extendedRcode,
636
+ version: version,
637
+ dnssecOk: dnssecOk,
638
+ rdata: rr.rdata,
639
+ };
640
+ }
641
+
642
+ function _resolveProfile(opts) {
643
+ if (opts.posture && COMPLIANCE_POSTURES[opts.posture]) {
644
+ return PROFILES[COMPLIANCE_POSTURES[opts.posture]];
645
+ }
646
+ var p = opts.profile || DEFAULT_PROFILE;
647
+ if (!PROFILES[p]) {
648
+ throw new SafeDnsError("safe-dns/bad-profile",
649
+ "safeDns: unknown profile '" + p + "' (valid: strict / balanced / permissive)");
650
+ }
651
+ return PROFILES[p];
652
+ }
653
+
654
+ module.exports = {
655
+ parseResponse: parseResponse,
656
+ boundEdns0: boundEdns0,
657
+ checkCnameChainDepth: checkCnameChainDepth,
658
+ compliancePosture: compliancePosture,
659
+ PROFILES: PROFILES,
660
+ COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
661
+ RTYPE_NAMES: RTYPE_NAMES,
662
+ SafeDnsError: SafeDnsError,
663
+ NAME: "dns",
664
+ KIND: "dns-response",
665
+ };