@blamejs/core 0.7.42 → 0.7.43
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 +2 -0
- package/lib/framework-error.js +9 -0
- package/lib/guard-all.js +1 -0
- package/lib/guard-domain.js +692 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.7.x
|
|
10
10
|
|
|
11
|
+
- **0.7.43** (2026-05-05) — `b.guardDomain` — domain-name identifier-safety primitive (KIND="identifier"). Validates user-supplied DNS names destined for allowlists, redirect targets, webhook endpoints, email-domain extraction, and CORS origin checks. Threat catalog: RFC 1035 §2.3.4 length caps (63 octets per label, 253 octets per FQDN), RFC 952 / 1123 LDH-rule violations (no leading/trailing hyphen, no `--` at positions 3-4 except `xn--`), IDN homograph mixed-script confusables (Latin / Cyrillic / Greek / Cherokee / Armenian / Han / Hiragana / Katakana / Hangul / Arabic / Hebrew range tables), BIDI / zero-width / control / null universal refuse via `lib/codepoint-class.js` (CVE-2021-42574 Trojan Source class), Punycode A-label malformation (bare `xn--`, double-encoded), RFC 6761 special-use suffix matching (`.localhost` / `.local` / `.invalid` / `.test` / `.onion` / `.alt` / `.home.arpa` / `.internal`), IPv4-as-domain confusion (CVE-2021-22931 — dotted-decimal / octal / hex / long-decimal forms), IPv6 bracket-literal, single-label / TLD-only refuse, wildcard `*` label refuse at every profile, RFC 8552 underscore-service-label policy, DGA Shannon-entropy heuristic for high-entropy long single labels (Mirai / Conficker C2 shape). Profiles: `strict` (Latin-only scripts, refuse-on-everything), `balanced` (audit Punycode, allow major international scripts, audit DGA), `permissive` (universal-refuse class still refused; everything else audit / allow). Compliance postures: `hipaa` / `pci-dss` / `soc2` strict overlay; `gdpr` balanced overlay. Auto-registers into `b.guardAll` as a STANDALONE_GUARD; the adaptive integration harness at `test/layer-5-integration/guard-host-integration.test.js` picks it up automatically. Defer-with-condition: full UTS #46 ToASCII / ToUnicode round-trip and Public-Suffix-List boundary enforcement ship behind operator-supplied callbacks (`opts.idnToAscii`, `opts.publicSuffixList`); re-open conditions are documented in the wiki page.
|
|
12
|
+
|
|
11
13
|
- **0.7.42** (2026-05-05) — gitleaks allowlist for v0.7.28 CHANGELOG doc-snippet false-positive. The v0.7.28 release-notes entry for `b.crypto.encryptMlkem768X25519` includes a JS-object-literal code example with a `privateKey: mlkemPrivateKey` field; gitleaks' default `generic-api-key` rule fires on the high-entropy content adjacent to the `privateKey:` token, blocking every release-tag CI run since v0.7.38. Allowlisted by commit + fingerprint per the same pattern as the existing `74e627e` entry — surgical suppression of the documented false positive, no broader rule weakening. Working-tree-only or rule-disabling alternatives were rejected: gitleaks' `git`-history scan is the stricter gate (catches secrets that landed and were later removed) and the documented snippet contains no real secret.
|
|
12
14
|
|
|
13
15
|
- **0.7.41** (2026-05-05) — wiki primitive-validator BACKLOG sweep: 50 of 101 entries pruned. Audited every entry in `examples/wiki/test/validate-primitive-sections.js`'s `UNDOCUMENTED_BACKLOG` map against the actual wiki page bodies; primitives that already have signature-form headings (`b.X.Y(...)`) discovered by the validator no longer need a BACKLOG entry suppressing them. Removed entries: `consent`, `websocketChannels`, `ssrfGuard`, `htmlBalance`, `csv`, `uuid`, `time`, `mailBounce`, `archive`, `breakGlass`, `forms`, `render`, `errorPage`, `cluster`, `safeBuffer`, `safeSql`, `safeUrl`, `retry`, `fileType`, `scheduler`, `jobs`, `backup`, `restore`, `i18n`, `cache`, `crypto`, `createApp`, `fileUpload`, `mtlsCa`, `pqcGate`, `pqcAgent`, `permissions`, `apiKey`, `webhook`, `notify`, `credentialHash`, `queue`, plus the 11-guard family (`guardEmail` through `guardAll`). The remaining 51 entries fall into three real classes: namespace-level entries with method-only headings (vault primitives, audit chain, etc.), pages structured around backend-builder patterns instead of flat methods (`objectStore`, `backupBundle`, etc.), and internal helpers that aren't part of the operator surface (`cli`, `boot`, `dev`, `protocolDispatcher`). The shrunk BACKLOG narrows the gap operators see between "covered by the parent's wiki page" and "actually drift waiting to be fixed."
|
package/index.js
CHANGED
|
@@ -110,6 +110,7 @@ var guardYaml = require("./lib/guard-yaml");
|
|
|
110
110
|
var guardXml = require("./lib/guard-xml");
|
|
111
111
|
var guardMarkdown = require("./lib/guard-markdown");
|
|
112
112
|
var guardEmail = require("./lib/guard-email");
|
|
113
|
+
var guardDomain = require("./lib/guard-domain");
|
|
113
114
|
var guardAll = require("./lib/guard-all");
|
|
114
115
|
var ssrfGuard = require("./lib/ssrf-guard");
|
|
115
116
|
var authHeader = require("./lib/auth-header");
|
|
@@ -251,6 +252,7 @@ module.exports = {
|
|
|
251
252
|
guardXml: guardXml,
|
|
252
253
|
guardMarkdown: guardMarkdown,
|
|
253
254
|
guardEmail: guardEmail,
|
|
255
|
+
guardDomain: guardDomain,
|
|
254
256
|
guardAll: guardAll,
|
|
255
257
|
ssrfGuard: ssrfGuard,
|
|
256
258
|
authHeader: authHeader,
|
package/lib/framework-error.js
CHANGED
|
@@ -249,6 +249,14 @@ var GuardMarkdownError = defineClass("GuardMarkdownError", { alwaysPermane
|
|
|
249
249
|
// in addresses, bidi/null/control chars in headers + addresses, header-
|
|
250
250
|
// folding smuggling, BOM injection. alwaysPermanent.
|
|
251
251
|
var GuardEmailError = defineClass("GuardEmailError", { alwaysPermanent: true });
|
|
252
|
+
// GuardDomainError covers domain-name identifier violations: RFC 1035
|
|
253
|
+
// length-cap overflow, RFC 952/1123 LDH-rule violations, IDN homograph /
|
|
254
|
+
// mixed-script confusables, BIDI / zero-width / control-byte injection,
|
|
255
|
+
// Punycode malformation, RFC 6761 special-use domains, IPv4-as-domain
|
|
256
|
+
// confusion (CVE-2021-22931), IPv6 bracket literals, single-label / TLD-
|
|
257
|
+
// only strings, wildcard labels, RFC 8552 underscore-label misuse, DGA
|
|
258
|
+
// high-entropy labels. alwaysPermanent.
|
|
259
|
+
var GuardDomainError = defineClass("GuardDomainError", { alwaysPermanent: true });
|
|
252
260
|
// DoraError covers DORA Article 17 incident-reporting workflow errors
|
|
253
261
|
// (classification refusal, report-shape validation, ESA-template
|
|
254
262
|
// generation, audit-chain integration). Permanent — these are
|
|
@@ -308,6 +316,7 @@ module.exports = {
|
|
|
308
316
|
GuardXmlError: GuardXmlError,
|
|
309
317
|
GuardMarkdownError: GuardMarkdownError,
|
|
310
318
|
GuardEmailError: GuardEmailError,
|
|
319
|
+
GuardDomainError: GuardDomainError,
|
|
311
320
|
DoraError: DoraError,
|
|
312
321
|
ComplianceError: ComplianceError,
|
|
313
322
|
SmtpPolicyError: SmtpPolicyError,
|
package/lib/guard-all.js
CHANGED
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* guard-domain — Domain-name identifier-safety primitive (b.guardDomain).
|
|
4
|
+
*
|
|
5
|
+
* Validates user-supplied DNS names destined for allowlists, redirect
|
|
6
|
+
* targets, webhook endpoints, email-domain extraction, and CORS origin
|
|
7
|
+
* checks. KIND="identifier" — consumes ctx.identifier (or ctx.domain).
|
|
8
|
+
*
|
|
9
|
+
* Threat catalog grounded in current research:
|
|
10
|
+
* - RFC 1035 §2.3.4 length caps — 63 octets / label, 253 octets / FQDN.
|
|
11
|
+
* - RFC 952 / 1123 LDH rule — letters / digits / hyphens; no leading/
|
|
12
|
+
* trailing hyphen; no `--` at positions 3-4 except `xn--` prefix.
|
|
13
|
+
* - IDN homograph / mixed-script confusables (RFC 5891-5894 IDNA2008,
|
|
14
|
+
* UTS #39). Cyrillic / Greek / Cherokee characters mixed with Latin
|
|
15
|
+
* in a single label spoof a trusted domain.
|
|
16
|
+
* - BIDI / RTL override (CVE-2021-42574 Trojan Source) in label
|
|
17
|
+
* codepoints — reorders visual presentation of the domain in
|
|
18
|
+
* address bars / log lines / audit records.
|
|
19
|
+
* - Zero-width / format codepoints — split a visible label into two
|
|
20
|
+
* parsed labels or hide characters from the operator.
|
|
21
|
+
* - Punycode `xn--` malformation — bare prefix that fails decode,
|
|
22
|
+
* double-encoded `xn--xn--`, U-label/A-label round-trip mismatch.
|
|
23
|
+
* - Special-use domain names (RFC 6761) — `.localhost`, `.local`,
|
|
24
|
+
* `.invalid`, `.example`, `.test`, `.onion`, `.alt`, `.home.arpa`,
|
|
25
|
+
* `.internal`. Allowlisting these as user-supplied webhook targets
|
|
26
|
+
* routes traffic to loopback or LAN.
|
|
27
|
+
* - IPv4-as-domain confusion (RFC 3986 §3.2.2 + CVE-2021-22931) —
|
|
28
|
+
* dotted-decimal, octal, hex, and long-decimal IPv4 forms accepted
|
|
29
|
+
* by `inet_aton`-style parsers but compared as strings by allowlist
|
|
30
|
+
* matchers.
|
|
31
|
+
* - IPv6 bracket-literal — same risk class.
|
|
32
|
+
* - TLD-only / single-label — resolves via search-domain suffix to
|
|
33
|
+
* attacker-chosen FQDN on misconfigured stubs.
|
|
34
|
+
* - Wildcard `*.example.com` — valid in TLS SAN and DNS but never in
|
|
35
|
+
* a user-input identifier.
|
|
36
|
+
* - Underscore labels — valid only for service-discovery prefixes
|
|
37
|
+
* per RFC 8552, never as a hostname.
|
|
38
|
+
* - Trailing dot — FQDN distinguisher; some libraries strip,
|
|
39
|
+
* some compare; allowlist mismatch.
|
|
40
|
+
* - DGA heuristic — high-entropy single-label domains
|
|
41
|
+
* (Mirai/Conficker family C2 indicator).
|
|
42
|
+
*
|
|
43
|
+
* var rv = b.guardDomain.validate("example.com", { profile: "strict" });
|
|
44
|
+
* var safe = b.guardDomain.sanitize("Example.Com.", { profile: "balanced" });
|
|
45
|
+
* var g = b.guardDomain.gate({ profile: "strict" });
|
|
46
|
+
*
|
|
47
|
+
* Defer-with-condition: full UTS #46 ToASCII / ToUnicode round-trip and
|
|
48
|
+
* Public-Suffix-List boundary enforcement ship behind operator-supplied
|
|
49
|
+
* callbacks (`opts.idnToAscii`, `opts.publicSuffixList`). Re-open
|
|
50
|
+
* conditions: an operator surfaces a use case for cookie-scope or
|
|
51
|
+
* email-domain canonicalization that needs framework-vendored PSL /
|
|
52
|
+
* UTS #46 tables.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
var codepointClass = require("./codepoint-class");
|
|
56
|
+
var lazyRequire = require("./lazy-require");
|
|
57
|
+
var gateContract = require("./gate-contract");
|
|
58
|
+
var C = require("./constants");
|
|
59
|
+
var numericBounds = require("./numeric-bounds");
|
|
60
|
+
var { GuardDomainError } = require("./framework-error");
|
|
61
|
+
|
|
62
|
+
var observability = lazyRequire(function () { return require("./observability"); });
|
|
63
|
+
void observability;
|
|
64
|
+
|
|
65
|
+
var _err = GuardDomainError.factory;
|
|
66
|
+
|
|
67
|
+
// ---- RFC 1035 §2.3.4 length caps ----
|
|
68
|
+
|
|
69
|
+
var LIMIT_LABEL_OCTETS = 63; // allow:raw-byte-literal — RFC 1035 §2.3.4
|
|
70
|
+
var LIMIT_DOMAIN_OCTETS = 253; // allow:raw-byte-literal — RFC 1035 §2.3.4 (255 wire minus length prefixes)
|
|
71
|
+
|
|
72
|
+
// ---- Static patterns (built from explicit codepoint tables) ----
|
|
73
|
+
|
|
74
|
+
// LDH label — letters / digits / hyphens, with leading-and-trailing
|
|
75
|
+
// hyphen rejection enforced separately. Length checked separately.
|
|
76
|
+
var LDH_LABEL_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/;
|
|
77
|
+
|
|
78
|
+
// Service-prefix label per RFC 8552 — `_dmarc`, `_acme-challenge`, …
|
|
79
|
+
var SERVICE_LABEL_RE = /^_[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/;
|
|
80
|
+
|
|
81
|
+
// Punycode A-label prefix.
|
|
82
|
+
var PUNYCODE_LABEL_RE = /^xn--/i;
|
|
83
|
+
|
|
84
|
+
// Bare `xn--` with no payload after — malformed A-label.
|
|
85
|
+
var BARE_XN_RE = /^xn--$/i;
|
|
86
|
+
|
|
87
|
+
// Wildcard label — `*` alone in any label position.
|
|
88
|
+
var WILDCARD_LABEL_RE = /^\*$/;
|
|
89
|
+
|
|
90
|
+
// IPv4 decimal-dotted form.
|
|
91
|
+
var IPV4_DOTTED_RE = /^(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$/;
|
|
92
|
+
|
|
93
|
+
// Looser IPv4 detection — every dot-segment is a numeric form
|
|
94
|
+
// (decimal, octal with leading 0, or hex with 0x prefix), or the whole
|
|
95
|
+
// input is a long-decimal / long-hex (no dots). Catches the parser-
|
|
96
|
+
// permissive forms `0177.0.0.1` (octal), `0xC0.0xA8.0x01.0x01` (hex),
|
|
97
|
+
// `3232235777` (long-decimal), `0xC0A80101` (long-hex).
|
|
98
|
+
//
|
|
99
|
+
// Detection requires at least one digit AND that every dot-segment is a
|
|
100
|
+
// number, so labels like `a.b` (purely alphabetic) don't false-positive
|
|
101
|
+
// as IPv4 even though their codepoints overlap the hex alphabet.
|
|
102
|
+
var IPV4_NUMERIC_SEGMENT_RE = /^(?:0[xX][0-9a-fA-F]+|[0-9]+)$/;
|
|
103
|
+
function _looksLikeIpv4Permissive(s) {
|
|
104
|
+
if (!/[0-9]/.test(s)) return false;
|
|
105
|
+
if (IPV4_NUMERIC_SEGMENT_RE.test(s)) {
|
|
106
|
+
// Long-decimal / long-hex without dots, e.g. `3232235777`.
|
|
107
|
+
return s.length > 0 && !/^[0-9]+$/.test(s) ? true :
|
|
108
|
+
// Pure long-decimal — at least 8 digits to count as IPv4
|
|
109
|
+
// representation, otherwise it's a port-shaped number.
|
|
110
|
+
s.length >= 8; // allow:raw-byte-literal — minimum digits to recognize long-decimal IPv4
|
|
111
|
+
}
|
|
112
|
+
if (s.indexOf(".") === -1) return false;
|
|
113
|
+
var parts = s.split(".");
|
|
114
|
+
if (parts.length !== 4) return false;
|
|
115
|
+
for (var i = 0; i < parts.length; i += 1) {
|
|
116
|
+
if (!IPV4_NUMERIC_SEGMENT_RE.test(parts[i])) return false;
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// IPv6 bracket-literal.
|
|
122
|
+
var IPV6_BRACKET_RE = /^\[[0-9a-fA-F:.]+\]$/;
|
|
123
|
+
|
|
124
|
+
// IDN script-range tables for mixed-script confusable detection. Same
|
|
125
|
+
// pattern guard-email uses; codepoints are numeric, never literal in
|
|
126
|
+
// source.
|
|
127
|
+
var SCRIPT_RANGES = {
|
|
128
|
+
latin: [[0x0041, 0x005A], [0x0061, 0x007A],
|
|
129
|
+
[0x00C0, 0x024F], [0x1E00, 0x1EFF]], // allow:raw-byte-literal — Unicode script ranges
|
|
130
|
+
cyrillic: [[0x0400, 0x04FF], [0x0500, 0x052F]], // allow:raw-byte-literal — Unicode Cyrillic + Cyrillic Supplement
|
|
131
|
+
greek: [[0x0370, 0x03FF], [0x1F00, 0x1FFF]], // allow:raw-byte-literal — Unicode Greek + Greek Extended
|
|
132
|
+
armenian: [[0x0530, 0x058F]], // allow:raw-byte-literal — Unicode Armenian
|
|
133
|
+
cherokee: [[0x13A0, 0x13FF], [0xAB70, 0xABBF]], // allow:raw-byte-literal — Unicode Cherokee + Cherokee Supplement
|
|
134
|
+
han: [[0x4E00, 0x9FFF]], // allow:raw-byte-literal — CJK Unified Ideographs
|
|
135
|
+
hiragana: [[0x3040, 0x309F]], // allow:raw-byte-literal — Hiragana
|
|
136
|
+
katakana: [[0x30A0, 0x30FF]], // allow:raw-byte-literal — Katakana
|
|
137
|
+
hangul: [[0xAC00, 0xD7AF]], // allow:raw-byte-literal — Hangul Syllables
|
|
138
|
+
arabic: [[0x0600, 0x06FF]], // allow:raw-byte-literal — Arabic
|
|
139
|
+
hebrew: [[0x0590, 0x05FF]], // allow:raw-byte-literal — Hebrew
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
function _scriptFor(cp) {
|
|
143
|
+
var keys = Object.keys(SCRIPT_RANGES);
|
|
144
|
+
for (var i = 0; i < keys.length; i += 1) {
|
|
145
|
+
var ranges = SCRIPT_RANGES[keys[i]];
|
|
146
|
+
for (var j = 0; j < ranges.length; j += 1) {
|
|
147
|
+
if (cp >= ranges[j][0] && cp <= ranges[j][1]) return keys[i];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function _detectMixedScripts(label, allowedScripts) {
|
|
154
|
+
var seen = {};
|
|
155
|
+
for (var i = 0; i < label.length; i += 1) {
|
|
156
|
+
var script = _scriptFor(label.charCodeAt(i));
|
|
157
|
+
if (script === null) continue;
|
|
158
|
+
seen[script] = true;
|
|
159
|
+
}
|
|
160
|
+
var scripts = Object.keys(seen);
|
|
161
|
+
if (scripts.length <= 1) return null;
|
|
162
|
+
if (!allowedScripts) return scripts;
|
|
163
|
+
var disallowed = [];
|
|
164
|
+
for (var k = 0; k < scripts.length; k += 1) {
|
|
165
|
+
if (allowedScripts.indexOf(scripts[k]) === -1) disallowed.push(scripts[k]);
|
|
166
|
+
}
|
|
167
|
+
return disallowed.length > 0 ? scripts : null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// RFC 6761 special-use domains + IETF reserved. Lowercase, no trailing
|
|
171
|
+
// dot. Match by suffix — `_acme-challenge.app.localhost` → `.localhost`.
|
|
172
|
+
//
|
|
173
|
+
// Excluded deliberately: `example.com` / `example.net` / `example.org`.
|
|
174
|
+
// Those are documentation-reserved but legitimately appear in test
|
|
175
|
+
// fixtures and SSO redirect-URI examples; refusing them at strict
|
|
176
|
+
// trips operators on benign inputs. A future `documentation-reserved`
|
|
177
|
+
// posture can flag them as warn-only when operators ask.
|
|
178
|
+
var SPECIAL_USE_DOMAINS = Object.freeze([
|
|
179
|
+
"localhost",
|
|
180
|
+
"local", // RFC 6762 mDNS
|
|
181
|
+
"invalid",
|
|
182
|
+
"test",
|
|
183
|
+
"onion", // RFC 7686
|
|
184
|
+
"alt", // RFC 9476
|
|
185
|
+
"home.arpa", // RFC 8375
|
|
186
|
+
"internal", // ICANN reserved 2024
|
|
187
|
+
]);
|
|
188
|
+
|
|
189
|
+
function _matchesSpecialUse(name) {
|
|
190
|
+
var lower = name.toLowerCase().replace(/\.$/, "");
|
|
191
|
+
for (var i = 0; i < SPECIAL_USE_DOMAINS.length; i += 1) {
|
|
192
|
+
var su = SPECIAL_USE_DOMAINS[i];
|
|
193
|
+
if (lower === su || lower.endsWith("." + su)) return su;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Shannon entropy in bits per character over a-z0-9 alphabet, used as
|
|
199
|
+
// a DGA heuristic. Returns 0 for trivial inputs.
|
|
200
|
+
function _shannonEntropy(s) {
|
|
201
|
+
if (!s || s.length < 2) return 0;
|
|
202
|
+
var counts = Object.create(null);
|
|
203
|
+
for (var i = 0; i < s.length; i += 1) {
|
|
204
|
+
var c = s.charAt(i).toLowerCase();
|
|
205
|
+
counts[c] = (counts[c] || 0) + 1;
|
|
206
|
+
}
|
|
207
|
+
var len = s.length;
|
|
208
|
+
var h = 0;
|
|
209
|
+
var keys = Object.keys(counts);
|
|
210
|
+
for (var k = 0; k < keys.length; k += 1) {
|
|
211
|
+
var p = counts[keys[k]] / len;
|
|
212
|
+
h -= p * Math.log2(p);
|
|
213
|
+
}
|
|
214
|
+
return h;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---- Profile presets ----
|
|
218
|
+
|
|
219
|
+
var PROFILES = Object.freeze({
|
|
220
|
+
"strict": {
|
|
221
|
+
bidiPolicy: "reject",
|
|
222
|
+
controlPolicy: "reject",
|
|
223
|
+
nullBytePolicy: "reject",
|
|
224
|
+
zeroWidthPolicy: "reject",
|
|
225
|
+
ldhPolicy: "reject",
|
|
226
|
+
underscorePolicy: "reject", // strict refuses service labels too
|
|
227
|
+
punycodePolicy: "reject",
|
|
228
|
+
mixedScriptPolicy: "reject",
|
|
229
|
+
specialUsePolicy: "reject",
|
|
230
|
+
ipLiteralPolicy: "reject",
|
|
231
|
+
wildcardPolicy: "reject",
|
|
232
|
+
singleLabelPolicy: "reject",
|
|
233
|
+
trailingDotPolicy: "normalize",
|
|
234
|
+
dgaPolicy: "reject",
|
|
235
|
+
allowedScripts: ["latin"],
|
|
236
|
+
dgaEntropyThreshold: 3.8, // allow:raw-byte-literal — Shannon entropy bits/char threshold (DGA heuristic)
|
|
237
|
+
dgaMinLabelLen: 12, // allow:raw-byte-literal — DGA heuristic floor
|
|
238
|
+
maxLabelOctets: LIMIT_LABEL_OCTETS,
|
|
239
|
+
maxDomainOctets: LIMIT_DOMAIN_OCTETS,
|
|
240
|
+
maxBytes: C.BYTES.bytes(2048),
|
|
241
|
+
maxRuntimeMs: C.TIME.seconds(2),
|
|
242
|
+
},
|
|
243
|
+
"balanced": {
|
|
244
|
+
bidiPolicy: "reject",
|
|
245
|
+
controlPolicy: "reject",
|
|
246
|
+
nullBytePolicy: "reject",
|
|
247
|
+
zeroWidthPolicy: "reject",
|
|
248
|
+
ldhPolicy: "reject",
|
|
249
|
+
underscorePolicy: "reject",
|
|
250
|
+
punycodePolicy: "audit",
|
|
251
|
+
mixedScriptPolicy: "reject",
|
|
252
|
+
specialUsePolicy: "reject",
|
|
253
|
+
ipLiteralPolicy: "reject",
|
|
254
|
+
wildcardPolicy: "reject",
|
|
255
|
+
singleLabelPolicy: "reject",
|
|
256
|
+
trailingDotPolicy: "normalize",
|
|
257
|
+
dgaPolicy: "audit",
|
|
258
|
+
allowedScripts: ["latin", "cyrillic", "greek", "han", "hiragana",
|
|
259
|
+
"katakana", "hangul"],
|
|
260
|
+
dgaEntropyThreshold: 3.8, // allow:raw-byte-literal — Shannon entropy bits/char threshold (DGA heuristic)
|
|
261
|
+
dgaMinLabelLen: 12, // allow:raw-byte-literal — DGA heuristic floor
|
|
262
|
+
maxLabelOctets: LIMIT_LABEL_OCTETS,
|
|
263
|
+
maxDomainOctets: LIMIT_DOMAIN_OCTETS,
|
|
264
|
+
maxBytes: C.BYTES.bytes(2048),
|
|
265
|
+
maxRuntimeMs: C.TIME.seconds(2),
|
|
266
|
+
},
|
|
267
|
+
"permissive": {
|
|
268
|
+
bidiPolicy: "reject", // BIDI refused at every profile — universal forgery
|
|
269
|
+
controlPolicy: "reject", // control bytes refused at every profile
|
|
270
|
+
nullBytePolicy: "reject", // null refused at every profile
|
|
271
|
+
zeroWidthPolicy: "reject", // zero-width refused at every profile — invisible label-segmentation
|
|
272
|
+
ldhPolicy: "audit",
|
|
273
|
+
underscorePolicy: "allow", // service labels permitted in permissive
|
|
274
|
+
punycodePolicy: "allow",
|
|
275
|
+
mixedScriptPolicy: "audit",
|
|
276
|
+
specialUsePolicy: "audit",
|
|
277
|
+
ipLiteralPolicy: "allow",
|
|
278
|
+
wildcardPolicy: "reject", // wildcard refused at every profile — never user-input
|
|
279
|
+
singleLabelPolicy: "audit",
|
|
280
|
+
trailingDotPolicy: "normalize",
|
|
281
|
+
dgaPolicy: "allow",
|
|
282
|
+
allowedScripts: null,
|
|
283
|
+
dgaEntropyThreshold: 3.8, // allow:raw-byte-literal — Shannon entropy bits/char threshold (DGA heuristic)
|
|
284
|
+
dgaMinLabelLen: 12, // allow:raw-byte-literal — DGA heuristic floor
|
|
285
|
+
maxLabelOctets: LIMIT_LABEL_OCTETS,
|
|
286
|
+
maxDomainOctets: LIMIT_DOMAIN_OCTETS,
|
|
287
|
+
maxBytes: C.BYTES.bytes(2048),
|
|
288
|
+
maxRuntimeMs: C.TIME.seconds(2),
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
var DEFAULTS = Object.freeze(Object.assign({}, PROFILES["strict"], {
|
|
293
|
+
mode: "enforce",
|
|
294
|
+
}));
|
|
295
|
+
|
|
296
|
+
var COMPLIANCE_POSTURES = Object.freeze({
|
|
297
|
+
"hipaa": Object.assign({}, PROFILES["strict"], {
|
|
298
|
+
forensicSnippetBytes: C.BYTES.bytes(256),
|
|
299
|
+
}),
|
|
300
|
+
"pci-dss": Object.assign({}, PROFILES["strict"], {
|
|
301
|
+
forensicSnippetBytes: C.BYTES.bytes(256),
|
|
302
|
+
}),
|
|
303
|
+
"gdpr": Object.assign({}, PROFILES["balanced"], {
|
|
304
|
+
forensicSnippetBytes: C.BYTES.bytes(128),
|
|
305
|
+
}),
|
|
306
|
+
"soc2": Object.assign({}, PROFILES["strict"], {
|
|
307
|
+
forensicSnippetBytes: C.BYTES.bytes(512),
|
|
308
|
+
}),
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
function _resolveOpts(opts) {
|
|
312
|
+
return gateContract.resolveProfileAndPosture(opts, {
|
|
313
|
+
profiles: PROFILES,
|
|
314
|
+
compliancePostures: COMPLIANCE_POSTURES,
|
|
315
|
+
defaults: DEFAULTS,
|
|
316
|
+
errorClass: GuardDomainError,
|
|
317
|
+
errCodePrefix: "domain",
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ---- Detection ----
|
|
322
|
+
|
|
323
|
+
function _detectIssues(input, opts) {
|
|
324
|
+
var issues = [];
|
|
325
|
+
if (typeof input !== "string") {
|
|
326
|
+
return [{ kind: "bad-input", severity: "high",
|
|
327
|
+
ruleId: "domain.bad-input",
|
|
328
|
+
snippet: "domain is not a string" }];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Total-length cap (UTF-8 byte count, not codepoint count, per RFC 1035).
|
|
332
|
+
var byteLen = Buffer.byteLength(input, "utf8");
|
|
333
|
+
if (byteLen > opts.maxDomainOctets) {
|
|
334
|
+
issues.push({
|
|
335
|
+
kind: "domain-cap", severity: "high",
|
|
336
|
+
ruleId: "domain.domain-cap",
|
|
337
|
+
snippet: "domain " + byteLen + " octets exceeds " +
|
|
338
|
+
opts.maxDomainOctets + " (RFC 1035 §2.3.4)",
|
|
339
|
+
});
|
|
340
|
+
return issues; // size cap is structural — abort further checks
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Codepoint-class threats (BIDI / control / null / zero-width). These
|
|
344
|
+
// are universal-refuse — running them first lets the more specific
|
|
345
|
+
// structural checks operate on a sanitized-or-refused input.
|
|
346
|
+
var charThreats = codepointClass.detectCharThreats(input, opts, "domain");
|
|
347
|
+
for (var ci = 0; ci < charThreats.length; ci += 1) issues.push(charThreats[ci]);
|
|
348
|
+
|
|
349
|
+
// Trailing-dot — FQDN distinguisher. Normalize for downstream checks
|
|
350
|
+
// but record as audit if operator wants to know.
|
|
351
|
+
var hadTrailingDot = input.charAt(input.length - 1) === ".";
|
|
352
|
+
var name = hadTrailingDot ? input.slice(0, -1) : input;
|
|
353
|
+
|
|
354
|
+
// Empty string after trim.
|
|
355
|
+
if (name.length === 0) {
|
|
356
|
+
issues.push({
|
|
357
|
+
kind: "empty", severity: "high",
|
|
358
|
+
ruleId: "domain.empty",
|
|
359
|
+
snippet: "domain is empty",
|
|
360
|
+
});
|
|
361
|
+
return issues;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Bracketed IPv6 literal.
|
|
365
|
+
if (IPV6_BRACKET_RE.test(name)) {
|
|
366
|
+
if (opts.ipLiteralPolicy !== "allow") {
|
|
367
|
+
issues.push({
|
|
368
|
+
kind: "ipv6-literal",
|
|
369
|
+
severity: opts.ipLiteralPolicy === "reject" ? "high" : "warn",
|
|
370
|
+
ruleId: "domain.ipv6-literal",
|
|
371
|
+
snippet: "input is an IPv6 bracket literal — bypasses DNS-name " +
|
|
372
|
+
"validation; pass through opts.allowIp if intended",
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
return issues;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// IPv4 detection — strict dotted-decimal AND loose (octal/hex/long).
|
|
379
|
+
if (IPV4_DOTTED_RE.test(name) || _looksLikeIpv4Permissive(name)) {
|
|
380
|
+
if (opts.ipLiteralPolicy !== "allow") {
|
|
381
|
+
issues.push({
|
|
382
|
+
kind: "ipv4-as-domain",
|
|
383
|
+
severity: opts.ipLiteralPolicy === "reject" ? "high" : "warn",
|
|
384
|
+
ruleId: "domain.ipv4-as-domain",
|
|
385
|
+
snippet: "input parses as IPv4 (CVE-2021-22931 class) — " +
|
|
386
|
+
"DNS-rebinding risk against allowlist matchers",
|
|
387
|
+
});
|
|
388
|
+
// Don't continue to label parsing — IPv4-shaped strings would
|
|
389
|
+
// collide with single-label / LDH errors and confuse the operator.
|
|
390
|
+
return issues;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// RFC 6761 special-use suffix.
|
|
395
|
+
var su = _matchesSpecialUse(name);
|
|
396
|
+
if (su && opts.specialUsePolicy !== "allow") {
|
|
397
|
+
issues.push({
|
|
398
|
+
kind: "special-use",
|
|
399
|
+
severity: opts.specialUsePolicy === "reject" ? "high" : "warn",
|
|
400
|
+
ruleId: "domain.special-use",
|
|
401
|
+
snippet: "domain matches RFC 6761 / IETF reserved suffix `." + su + "` " +
|
|
402
|
+
"— would route to loopback / mDNS / Tor / LAN",
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Label split + per-label structural checks.
|
|
407
|
+
var labels = name.split(".");
|
|
408
|
+
|
|
409
|
+
// Single-label / TLD-only.
|
|
410
|
+
if (labels.length < 2) {
|
|
411
|
+
if (opts.singleLabelPolicy !== "allow") {
|
|
412
|
+
issues.push({
|
|
413
|
+
kind: "single-label",
|
|
414
|
+
severity: opts.singleLabelPolicy === "reject" ? "high" : "warn",
|
|
415
|
+
ruleId: "domain.single-label",
|
|
416
|
+
snippet: "single-label / TLD-only domain — risks search-domain " +
|
|
417
|
+
"suffixing on misconfigured stub resolvers",
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
for (var li = 0; li < labels.length; li += 1) {
|
|
423
|
+
var label = labels[li];
|
|
424
|
+
|
|
425
|
+
// Empty label (e.g. `foo..bar` or leading `.foo`).
|
|
426
|
+
if (label.length === 0) {
|
|
427
|
+
issues.push({
|
|
428
|
+
kind: "empty-label", severity: "high",
|
|
429
|
+
ruleId: "domain.empty-label",
|
|
430
|
+
snippet: "label " + (li + 1) + " is empty (consecutive or " +
|
|
431
|
+
"leading dots)",
|
|
432
|
+
});
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
var labelBytes = Buffer.byteLength(label, "utf8");
|
|
437
|
+
if (labelBytes > opts.maxLabelOctets) {
|
|
438
|
+
issues.push({
|
|
439
|
+
kind: "label-cap", severity: "high",
|
|
440
|
+
ruleId: "domain.label-cap",
|
|
441
|
+
snippet: "label " + (li + 1) + " is " + labelBytes +
|
|
442
|
+
" octets, exceeds " + opts.maxLabelOctets +
|
|
443
|
+
" (RFC 1035 §2.3.4)",
|
|
444
|
+
});
|
|
445
|
+
continue; // label-cap masks downstream rule failures
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Wildcard `*`.
|
|
449
|
+
if (WILDCARD_LABEL_RE.test(label)) { // allow:regex-no-length-cap — label bounded by maxLabelOctets above
|
|
450
|
+
if (opts.wildcardPolicy !== "allow") {
|
|
451
|
+
issues.push({
|
|
452
|
+
kind: "wildcard", severity: "high",
|
|
453
|
+
ruleId: "domain.wildcard",
|
|
454
|
+
snippet: "wildcard label `*` — valid in TLS SAN / DNS RR but " +
|
|
455
|
+
"never in a user-input identifier",
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Service-prefix label (RFC 8552). Underscore allowed only if
|
|
462
|
+
// operator opts in.
|
|
463
|
+
if (label.charAt(0) === "_") {
|
|
464
|
+
if (SERVICE_LABEL_RE.test(label)) { // allow:regex-no-length-cap — label bounded by maxLabelOctets above
|
|
465
|
+
if (opts.underscorePolicy !== "allow") {
|
|
466
|
+
issues.push({
|
|
467
|
+
kind: "underscore-label",
|
|
468
|
+
severity: opts.underscorePolicy === "reject" ? "high" : "warn",
|
|
469
|
+
ruleId: "domain.underscore-label",
|
|
470
|
+
snippet: "label " + (li + 1) + " starts with `_` (RFC 8552 " +
|
|
471
|
+
"service label) — never valid as a hostname",
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
issues.push({
|
|
476
|
+
kind: "underscore-malformed", severity: "high",
|
|
477
|
+
ruleId: "domain.underscore-malformed",
|
|
478
|
+
snippet: "label " + (li + 1) + " starts with `_` but doesn't " +
|
|
479
|
+
"match the service-label grammar",
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Punycode A-label.
|
|
486
|
+
if (PUNYCODE_LABEL_RE.test(label)) { // allow:regex-no-length-cap — label bounded by maxLabelOctets above
|
|
487
|
+
if (BARE_XN_RE.test(label)) { // allow:regex-no-length-cap — label bounded by maxLabelOctets above
|
|
488
|
+
issues.push({
|
|
489
|
+
kind: "punycode-bare", severity: "high",
|
|
490
|
+
ruleId: "domain.punycode-bare",
|
|
491
|
+
snippet: "label " + (li + 1) + " is bare `xn--` with no " +
|
|
492
|
+
"Punycode payload",
|
|
493
|
+
});
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (opts.punycodePolicy !== "allow") {
|
|
497
|
+
issues.push({
|
|
498
|
+
kind: "punycode-label",
|
|
499
|
+
severity: opts.punycodePolicy === "reject" ? "high" : "warn",
|
|
500
|
+
ruleId: "domain.punycode-label",
|
|
501
|
+
snippet: "label " + (li + 1) + " is an IDN A-label (`xn--`) — " +
|
|
502
|
+
"homograph-spoofing class without round-trip validation",
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
// ASCII LDH check still applies.
|
|
506
|
+
if (!LDH_LABEL_RE.test(label) && opts.ldhPolicy !== "allow") { // allow:regex-no-length-cap — label bounded by maxLabelOctets above
|
|
507
|
+
issues.push({
|
|
508
|
+
kind: "ldh-violation", severity: "high",
|
|
509
|
+
ruleId: "domain.ldh-violation",
|
|
510
|
+
snippet: "label " + (li + 1) + " (Punycode form) violates LDH " +
|
|
511
|
+
"rule (RFC 952 / 1123 §2.1)",
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ASCII LDH or Unicode label.
|
|
518
|
+
var allAscii = true;
|
|
519
|
+
for (var ai = 0; ai < label.length; ai += 1) {
|
|
520
|
+
if (label.charCodeAt(ai) > 0x7F) { allAscii = false; break; } // allow:raw-byte-literal — ASCII boundary codepoint
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (allAscii) {
|
|
524
|
+
if (!LDH_LABEL_RE.test(label) && opts.ldhPolicy !== "allow") { // allow:regex-no-length-cap — label bounded by maxLabelOctets above
|
|
525
|
+
issues.push({
|
|
526
|
+
kind: "ldh-violation",
|
|
527
|
+
severity: opts.ldhPolicy === "reject" ? "high" : "warn",
|
|
528
|
+
ruleId: "domain.ldh-violation",
|
|
529
|
+
snippet: "label " + (li + 1) + " " + JSON.stringify(label) +
|
|
530
|
+
" violates LDH rule (RFC 952 / 1123 §2.1)",
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
// Position-3-4 double-hyphen check excluding the `xn--` prefix.
|
|
534
|
+
if (label.length >= 4 && label.charAt(2) === "-" &&
|
|
535
|
+
label.charAt(3) === "-" && !PUNYCODE_LABEL_RE.test(label)) {
|
|
536
|
+
issues.push({
|
|
537
|
+
kind: "double-hyphen", severity: "warn",
|
|
538
|
+
ruleId: "domain.double-hyphen",
|
|
539
|
+
snippet: "label " + (li + 1) + " has `--` at positions 3-4 " +
|
|
540
|
+
"without the `xn--` IDN prefix",
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
544
|
+
// Unicode label — flag mixed-script confusables and strict-LDH
|
|
545
|
+
// operators that didn't opt into IDN.
|
|
546
|
+
if (opts.punycodePolicy !== "allow") {
|
|
547
|
+
// Operator wants Punycode-only; reject raw Unicode labels.
|
|
548
|
+
issues.push({
|
|
549
|
+
kind: "raw-unicode-label",
|
|
550
|
+
severity: opts.punycodePolicy === "reject" ? "high" : "warn",
|
|
551
|
+
ruleId: "domain.raw-unicode-label",
|
|
552
|
+
snippet: "label " + (li + 1) + " contains raw Unicode " +
|
|
553
|
+
"(non-ASCII) — IDN labels must be Punycode-encoded " +
|
|
554
|
+
"(`xn--…`) for transport-safe comparison",
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
var mixed = _detectMixedScripts(label, opts.allowedScripts);
|
|
558
|
+
if (mixed && opts.mixedScriptPolicy !== "allow") {
|
|
559
|
+
issues.push({
|
|
560
|
+
kind: "mixed-script",
|
|
561
|
+
severity: opts.mixedScriptPolicy === "reject" ? "critical" : "high",
|
|
562
|
+
ruleId: "domain.mixed-script",
|
|
563
|
+
snippet: "label " + (li + 1) + " mixes scripts (" +
|
|
564
|
+
mixed.join(", ") + ") — IDN homograph spoofing class",
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// DGA entropy heuristic — high-entropy long single label is C2-shape.
|
|
570
|
+
if (label.length >= opts.dgaMinLabelLen && opts.dgaPolicy !== "allow") {
|
|
571
|
+
var h = _shannonEntropy(label);
|
|
572
|
+
if (h >= opts.dgaEntropyThreshold) {
|
|
573
|
+
issues.push({
|
|
574
|
+
kind: "dga-entropy",
|
|
575
|
+
severity: opts.dgaPolicy === "reject" ? "high" : "warn",
|
|
576
|
+
ruleId: "domain.dga-entropy",
|
|
577
|
+
snippet: "label " + (li + 1) + " has Shannon entropy " +
|
|
578
|
+
h.toFixed(2) + " bits/char (>= " +
|
|
579
|
+
opts.dgaEntropyThreshold + ") — C2 / DGA shape",
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Trailing-dot audit signal (after structural checks, before return).
|
|
586
|
+
if (hadTrailingDot && opts.trailingDotPolicy === "audit") {
|
|
587
|
+
issues.push({
|
|
588
|
+
kind: "trailing-dot", severity: "warn",
|
|
589
|
+
ruleId: "domain.trailing-dot",
|
|
590
|
+
snippet: "input had trailing dot (FQDN-marker) — normalize/strip " +
|
|
591
|
+
"before allowlist comparison",
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return issues;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function validate(input, opts) {
|
|
599
|
+
opts = _resolveOpts(opts);
|
|
600
|
+
numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
|
|
601
|
+
["maxLabelOctets", "maxDomainOctets", "maxBytes", "dgaMinLabelLen"],
|
|
602
|
+
"guardDomain.validate", GuardDomainError, "domain.bad-opt");
|
|
603
|
+
if (typeof input !== "string") {
|
|
604
|
+
return {
|
|
605
|
+
ok: false,
|
|
606
|
+
issues: [{ kind: "bad-input", severity: "high",
|
|
607
|
+
ruleId: "domain.bad-input",
|
|
608
|
+
snippet: "domain is not a string" }],
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
return gateContract.aggregateIssues(_detectIssues(input, opts));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function sanitize(input, opts) {
|
|
615
|
+
opts = _resolveOpts(opts);
|
|
616
|
+
if (typeof input !== "string") {
|
|
617
|
+
throw _err("domain.bad-input", "sanitize requires string input");
|
|
618
|
+
}
|
|
619
|
+
// Critical refuses can't be repaired.
|
|
620
|
+
var issues = _detectIssues(input, opts);
|
|
621
|
+
for (var i = 0; i < issues.length; i += 1) {
|
|
622
|
+
if (issues[i].severity === "critical" || issues[i].severity === "high") {
|
|
623
|
+
throw _err(issues[i].ruleId || "domain.refused",
|
|
624
|
+
"guardDomain.sanitize: " + issues[i].snippet);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
// Safe transforms: lowercase ASCII, strip trailing dot.
|
|
628
|
+
var out = input.toLowerCase();
|
|
629
|
+
if (out.charAt(out.length - 1) === ".") out = out.slice(0, -1);
|
|
630
|
+
return out;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function gate(opts) {
|
|
634
|
+
opts = _resolveOpts(opts);
|
|
635
|
+
return gateContract.buildGuardGate(
|
|
636
|
+
opts.name || "guardDomain:" + (opts.profile || "default"),
|
|
637
|
+
opts,
|
|
638
|
+
async function (ctx) {
|
|
639
|
+
// Identifier-shape ctx — operator passes via ctx.identifier or
|
|
640
|
+
// ctx.domain.
|
|
641
|
+
var identifier = ctx && (ctx.identifier || ctx.domain || "");
|
|
642
|
+
if (!identifier) return { ok: true, action: "serve" };
|
|
643
|
+
var rv = validate(identifier, opts);
|
|
644
|
+
if (rv.issues.length === 0) return { ok: true, action: "serve" };
|
|
645
|
+
var hasCritical = rv.issues.some(function (i) {
|
|
646
|
+
return i.severity === "critical";
|
|
647
|
+
});
|
|
648
|
+
var hasHigh = rv.issues.some(function (i) {
|
|
649
|
+
return i.severity === "high";
|
|
650
|
+
});
|
|
651
|
+
if (!hasCritical && !hasHigh) {
|
|
652
|
+
return { ok: true, action: "audit-only", issues: rv.issues };
|
|
653
|
+
}
|
|
654
|
+
return { ok: false, action: "refuse", issues: rv.issues };
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
var buildProfile = gateContract.makeProfileBuilder(PROFILES);
|
|
659
|
+
|
|
660
|
+
function compliancePosture(name) {
|
|
661
|
+
return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES,
|
|
662
|
+
_err, "domain");
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
var _domainRulePacks = gateContract.makeRulePackLoader(GuardDomainError, "domain");
|
|
666
|
+
var loadRulePack = _domainRulePacks.load;
|
|
667
|
+
|
|
668
|
+
module.exports = {
|
|
669
|
+
// ---- guard-* family registry exports ----
|
|
670
|
+
NAME: "domain",
|
|
671
|
+
KIND: "identifier",
|
|
672
|
+
INTEGRATION_FIXTURES: Object.freeze({
|
|
673
|
+
kind: "identifier",
|
|
674
|
+
benignBytes: Buffer.from("example.com", "utf8"),
|
|
675
|
+
// Hostile: dotted-decimal IPv4 (CVE-2021-22931 class) — every
|
|
676
|
+
// profile refuses (allowlist-bypass via DNS rebinding).
|
|
677
|
+
hostileBytes: Buffer.from("192.168.1.1", "utf8"),
|
|
678
|
+
benignIdentifier: "example.com",
|
|
679
|
+
hostileIdentifier: "192.168.1.1",
|
|
680
|
+
}),
|
|
681
|
+
// ---- primitive surface ----
|
|
682
|
+
validate: validate,
|
|
683
|
+
sanitize: sanitize,
|
|
684
|
+
gate: gate,
|
|
685
|
+
buildProfile: buildProfile,
|
|
686
|
+
compliancePosture: compliancePosture,
|
|
687
|
+
loadRulePack: loadRulePack,
|
|
688
|
+
PROFILES: PROFILES,
|
|
689
|
+
DEFAULTS: DEFAULTS,
|
|
690
|
+
COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
|
|
691
|
+
GuardDomainError: GuardDomainError,
|
|
692
|
+
};
|
package/package.json
CHANGED
package/sbom.cyclonedx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:5c9d460f-9bbe-4b28-8506-6d5ecf7f0090",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-05T20:
|
|
8
|
+
"timestamp": "2026-05-05T20:56:37.841Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.7.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.7.43",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.7.
|
|
25
|
+
"version": "0.7.43",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.7.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.7.43",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.7.
|
|
57
|
+
"ref": "@blamejs/core@0.7.43",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|