@blamejs/core 0.8.42 → 0.8.49
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 +93 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/safe-url.js
CHANGED
|
@@ -1,64 +1,186 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* (operator passed a WebSocket URL to a non-WebSocket client). Each
|
|
46
|
-
* caller declares its own narrow allowlist; an off-protocol URL
|
|
47
|
-
* fails with a clear "protocol not allowed here" error rather than
|
|
48
|
-
* trying and failing weirdly later.
|
|
3
|
+
* @module b.safeUrl
|
|
4
|
+
* @nav Validation
|
|
5
|
+
* @title Safe Url
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Defensive URL parsing with a protocol allowlist (HTTPS-only by
|
|
9
|
+
* default), authority validation, IDN-homograph defense, and a
|
|
10
|
+
* length cap that runs BEFORE Node's WHATWG URL parser sees the
|
|
11
|
+
* input. The framework's stance on outbound URLs: TLS-required by
|
|
12
|
+
* default; cleartext (`http:` / `ws:`) is opt-in per call via
|
|
13
|
+
* `opts.allowedProtocols`. `user:pass@` userinfo refuses by
|
|
14
|
+
* default — credentials belong in headers / a credential store,
|
|
15
|
+
* not in URL strings that leak into request logs, error messages,
|
|
16
|
+
* metric labels, and trace spans. Mixed-script host labels
|
|
17
|
+
* (Cyrillic 'о' inside an otherwise-Latin label, etc. — UTS #39 §5
|
|
18
|
+
* homograph shape) refuse by default and emit
|
|
19
|
+
* `safeurl.idn_homograph.refused` to the audit chain so a forensic
|
|
20
|
+
* review can reconstruct every accepted host.
|
|
21
|
+
*
|
|
22
|
+
* Pre-baked protocol allowlists are exposed as frozen arrays so
|
|
23
|
+
* each caller can declare a NARROW per-call allowlist (the
|
|
24
|
+
* http-client speaks HTTP, not WebSocket; a `wss://` URL handed to
|
|
25
|
+
* it is a category error that should fail loudly here, not later
|
|
26
|
+
* inside a transport):
|
|
27
|
+
*
|
|
28
|
+
* ALLOW_HTTP_TLS ["https:"] (secure HTTP default)
|
|
29
|
+
* ALLOW_HTTP_ALL ["http:", "https:"] (HTTP + cleartext opt-in)
|
|
30
|
+
* ALLOW_WS_TLS ["wss:"] (secure WS default)
|
|
31
|
+
* ALLOW_WS_ALL ["ws:", "wss:"] (WS + cleartext opt-in)
|
|
32
|
+
* ALLOW_ANY ["http:", "https:", "ws:", "wss:"]
|
|
33
|
+
*
|
|
34
|
+
* `parse` throws `SafeUrlError` (or a caller-supplied error class
|
|
35
|
+
* via `opts.errorClass`, used by `b.objectStore` / `b.logStream` /
|
|
36
|
+
* `b.httpClient` to surface their own decorated error type) with a
|
|
37
|
+
* stable `.code`: `safe-url/missing` / `safe-url/too-long` /
|
|
38
|
+
* `safe-url/malformed` / `safe-url/protocol-disallowed` /
|
|
39
|
+
* `safe-url/userinfo-disallowed` / `safe-url/idn-homograph` /
|
|
40
|
+
* `safe-url/bad-opt`. Operator code that wants a boolean
|
|
41
|
+
* parse-without-throw shape wraps the throw in a try / catch.
|
|
42
|
+
*
|
|
43
|
+
* @card
|
|
44
|
+
* Defensive URL parsing with a protocol allowlist (HTTPS-only by default), authority validation, IDN-homograph defense, and a length cap that runs BEFORE Node's WHATWG URL parser sees the input.
|
|
49
45
|
*/
|
|
50
46
|
|
|
51
47
|
var C = require("./constants");
|
|
48
|
+
var codepointClass = require("./codepoint-class");
|
|
49
|
+
var lazyRequire = require("./lazy-require");
|
|
52
50
|
var numericBounds = require("./numeric-bounds");
|
|
53
51
|
var { FrameworkError } = require("./framework-error");
|
|
52
|
+
var nodeUrl = require("url");
|
|
54
53
|
var { URL } = require("url");
|
|
55
54
|
|
|
55
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @primitive b.safeUrl.ALLOW_HTTP_TLS
|
|
59
|
+
* @signature b.safeUrl.ALLOW_HTTP_TLS
|
|
60
|
+
* @since 0.1.0
|
|
61
|
+
* @status stable
|
|
62
|
+
* @related b.safeUrl.parse, b.safeUrl.ALLOW_HTTP_ALL
|
|
63
|
+
*
|
|
64
|
+
* Frozen protocol allowlist for HTTPS-only HTTP traffic — `["https:"]`.
|
|
65
|
+
* The framework default for any outbound URL parsed without an
|
|
66
|
+
* explicit `opts.allowedProtocols`. Operators with a legitimate
|
|
67
|
+
* cleartext use case opt in per call via `ALLOW_HTTP_ALL`.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* var b = require("blamejs");
|
|
71
|
+
* b.safeUrl.ALLOW_HTTP_TLS;
|
|
72
|
+
* // → ["https:"]
|
|
73
|
+
*/
|
|
56
74
|
var ALLOW_HTTP_TLS = Object.freeze(["https:"]);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @primitive b.safeUrl.ALLOW_HTTP_ALL
|
|
78
|
+
* @signature b.safeUrl.ALLOW_HTTP_ALL
|
|
79
|
+
* @since 0.1.0
|
|
80
|
+
* @status stable
|
|
81
|
+
* @related b.safeUrl.parse, b.safeUrl.ALLOW_HTTP_TLS
|
|
82
|
+
*
|
|
83
|
+
* Frozen protocol allowlist accepting both HTTP and HTTPS —
|
|
84
|
+
* `["http:", "https:"]`. Pass to `parse` when the call site
|
|
85
|
+
* legitimately speaks cleartext (loopback admin endpoints, on-prem
|
|
86
|
+
* service mesh terminating TLS at a sidecar, legacy partner APIs).
|
|
87
|
+
* Never the framework default — TLS-required is.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* var b = require("blamejs");
|
|
91
|
+
* var u = b.safeUrl.parse("http://127.0.0.1:8080/health", {
|
|
92
|
+
* allowedProtocols: b.safeUrl.ALLOW_HTTP_ALL,
|
|
93
|
+
* });
|
|
94
|
+
* u.protocol;
|
|
95
|
+
* // → "http:"
|
|
96
|
+
*/
|
|
57
97
|
var ALLOW_HTTP_ALL = Object.freeze(["http:", "https:"]);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @primitive b.safeUrl.ALLOW_WS_TLS
|
|
101
|
+
* @signature b.safeUrl.ALLOW_WS_TLS
|
|
102
|
+
* @since 0.1.0
|
|
103
|
+
* @status stable
|
|
104
|
+
* @related b.safeUrl.parse, b.safeUrl.ALLOW_WS_ALL
|
|
105
|
+
*
|
|
106
|
+
* Frozen protocol allowlist for secure WebSocket traffic — `["wss:"]`.
|
|
107
|
+
* The framework default for any WebSocket URL parsed without an
|
|
108
|
+
* explicit `opts.allowedProtocols`.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* var b = require("blamejs");
|
|
112
|
+
* b.safeUrl.ALLOW_WS_TLS;
|
|
113
|
+
* // → ["wss:"]
|
|
114
|
+
*/
|
|
58
115
|
var ALLOW_WS_TLS = Object.freeze(["wss:"]);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @primitive b.safeUrl.ALLOW_WS_ALL
|
|
119
|
+
* @signature b.safeUrl.ALLOW_WS_ALL
|
|
120
|
+
* @since 0.1.0
|
|
121
|
+
* @status stable
|
|
122
|
+
* @related b.safeUrl.parse, b.safeUrl.ALLOW_WS_TLS
|
|
123
|
+
*
|
|
124
|
+
* Frozen protocol allowlist accepting both `ws:` and `wss:` —
|
|
125
|
+
* `["ws:", "wss:"]`. Opt-in per call when cleartext WebSocket is
|
|
126
|
+
* acceptable (loopback dev, sidecar-terminated TLS).
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* var b = require("blamejs");
|
|
130
|
+
* var u = b.safeUrl.parse("ws://127.0.0.1:9000/stream", {
|
|
131
|
+
* allowedProtocols: b.safeUrl.ALLOW_WS_ALL,
|
|
132
|
+
* });
|
|
133
|
+
* u.protocol;
|
|
134
|
+
* // → "ws:"
|
|
135
|
+
*/
|
|
59
136
|
var ALLOW_WS_ALL = Object.freeze(["ws:", "wss:"]);
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @primitive b.safeUrl.ALLOW_ANY
|
|
140
|
+
* @signature b.safeUrl.ALLOW_ANY
|
|
141
|
+
* @since 0.1.0
|
|
142
|
+
* @status stable
|
|
143
|
+
* @related b.safeUrl.parse, b.safeUrl.ALLOW_HTTP_TLS
|
|
144
|
+
*
|
|
145
|
+
* Frozen allowlist accepting every framework-supported scheme —
|
|
146
|
+
* `["http:", "https:", "ws:", "wss:"]`. Suited to a generic
|
|
147
|
+
* URL-validation surface where the caller already enforces the
|
|
148
|
+
* protocol downstream; narrower allowlists are preferred wherever
|
|
149
|
+
* possible.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* var b = require("blamejs");
|
|
153
|
+
* b.safeUrl.ALLOW_ANY.length;
|
|
154
|
+
* // → 4
|
|
155
|
+
*/
|
|
60
156
|
var ALLOW_ANY = Object.freeze(["http:", "https:", "ws:", "wss:"]);
|
|
61
157
|
|
|
158
|
+
/**
|
|
159
|
+
* @primitive b.safeUrl.SafeUrlError
|
|
160
|
+
* @signature b.safeUrl.SafeUrlError
|
|
161
|
+
* @since 0.1.0
|
|
162
|
+
* @status stable
|
|
163
|
+
* @related b.safeUrl.parse
|
|
164
|
+
*
|
|
165
|
+
* Error class thrown by `parse` (or by the caller-supplied
|
|
166
|
+
* `opts.errorClass`, used by `b.objectStore` / `b.logStream` /
|
|
167
|
+
* `b.httpClient` to surface a decorated operational error type).
|
|
168
|
+
* Extends `FrameworkError`. Carries a stable `.code`:
|
|
169
|
+
* `safe-url/missing` / `safe-url/too-long` / `safe-url/malformed` /
|
|
170
|
+
* `safe-url/protocol-disallowed` / `safe-url/userinfo-disallowed` /
|
|
171
|
+
* `safe-url/idn-homograph` / `safe-url/bad-opt`. HTTP middleware
|
|
172
|
+
* inspects `.code` to translate the throw into a 400 without
|
|
173
|
+
* leaking parser internals.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* var b = require("blamejs");
|
|
177
|
+
* try {
|
|
178
|
+
* b.safeUrl.parse("ftp://example.com/file.txt");
|
|
179
|
+
* } catch (e) {
|
|
180
|
+
* e instanceof b.safeUrl.SafeUrlError; // → true
|
|
181
|
+
* e.code; // → "safe-url/protocol-disallowed"
|
|
182
|
+
* }
|
|
183
|
+
*/
|
|
62
184
|
class SafeUrlError extends FrameworkError {
|
|
63
185
|
constructor(code, message) {
|
|
64
186
|
super(message, code);
|
|
@@ -83,6 +205,71 @@ function _makeError(errorClass, code, message) {
|
|
|
83
205
|
// payloads) override via opts.maxUrlLength.
|
|
84
206
|
var DEFAULT_MAX_URL_LENGTH = C.BYTES.kib(8);
|
|
85
207
|
|
|
208
|
+
/**
|
|
209
|
+
* @primitive b.safeUrl.parse
|
|
210
|
+
* @signature b.safeUrl.parse(url, opts?)
|
|
211
|
+
* @since 0.1.0
|
|
212
|
+
* @status stable
|
|
213
|
+
* @related b.safeUrl.SafeUrlError, b.safeUrl.ALLOW_HTTP_TLS, b.safeUrl.ALLOW_HTTP_ALL
|
|
214
|
+
*
|
|
215
|
+
* Parse a URL string (or an existing `URL` instance) through the
|
|
216
|
+
* framework's defensive gates: length cap BEFORE Node's WHATWG parser
|
|
217
|
+
* sees the input (RFC 7230 §3.1.1 — 8 KiB default), protocol
|
|
218
|
+
* allowlist (`https:` only by default), `user:pass@` userinfo refusal
|
|
219
|
+
* (credentials leak into request logs / error messages / metric
|
|
220
|
+
* labels / trace spans), and per-label IDN-homograph defense
|
|
221
|
+
* (UTS #39 §5 mixed-script — Cyrillic 'о' inside an otherwise-Latin
|
|
222
|
+
* label). Returns the parsed `URL` instance on success.
|
|
223
|
+
*
|
|
224
|
+
* Throws `SafeUrlError` (or the caller-supplied `opts.errorClass`)
|
|
225
|
+
* with one of the documented `.code` strings: `safe-url/missing` /
|
|
226
|
+
* `safe-url/too-long` / `safe-url/malformed` /
|
|
227
|
+
* `safe-url/protocol-disallowed` / `safe-url/userinfo-disallowed` /
|
|
228
|
+
* `safe-url/idn-homograph` / `safe-url/bad-opt`. Operator code that
|
|
229
|
+
* wants a boolean parse-without-throw shape wraps the call in a
|
|
230
|
+
* `try` / `catch`.
|
|
231
|
+
*
|
|
232
|
+
* @opts
|
|
233
|
+
* allowedProtocols: string[], // default ALLOW_HTTP_TLS (["https:"])
|
|
234
|
+
* maxUrlLength: number, // default 8192 (RFC 7230 §3.1.1)
|
|
235
|
+
* allowUserinfo: boolean, // default false; opt-in to user:pass@
|
|
236
|
+
* allowMixedScript: boolean, // default false; opt-in to mixed-script labels
|
|
237
|
+
* allowedScripts: string[], // narrow mixed-script allowlist (e.g. ["latin","cyrillic"])
|
|
238
|
+
* errorClass: Function, // throw this instead of SafeUrlError (used by b.httpClient / b.objectStore)
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* var b = require("blamejs");
|
|
242
|
+
*
|
|
243
|
+
* // Default: HTTPS-only, length cap, userinfo refused, IDN-homograph defended.
|
|
244
|
+
* var u = b.safeUrl.parse("https://example.com/path?q=1");
|
|
245
|
+
* u.hostname;
|
|
246
|
+
* // → "example.com"
|
|
247
|
+
*
|
|
248
|
+
* // Cleartext is opt-in per call via the ALLOW_HTTP_ALL preset.
|
|
249
|
+
* var http = b.safeUrl.parse("http://127.0.0.1:8080/health", {
|
|
250
|
+
* allowedProtocols: b.safeUrl.ALLOW_HTTP_ALL,
|
|
251
|
+
* });
|
|
252
|
+
* http.protocol;
|
|
253
|
+
* // → "http:"
|
|
254
|
+
*
|
|
255
|
+
* // Disallowed protocol throws SafeUrlError.
|
|
256
|
+
* try { b.safeUrl.parse("javascript:alert(1)"); }
|
|
257
|
+
* catch (e) { e.code; }
|
|
258
|
+
* // → "safe-url/protocol-disallowed"
|
|
259
|
+
*
|
|
260
|
+
* // Userinfo refused by default — credentials belong in headers.
|
|
261
|
+
* try { b.safeUrl.parse("https://alice:s3cr3t@example.com/"); }
|
|
262
|
+
* catch (e) { e.code; }
|
|
263
|
+
* // → "safe-url/userinfo-disallowed"
|
|
264
|
+
*
|
|
265
|
+
* // Boolean parse-without-throw shape via try/catch wrapper.
|
|
266
|
+
* function isValid(s) {
|
|
267
|
+
* try { b.safeUrl.parse(s); return true; }
|
|
268
|
+
* catch (_e) { return false; }
|
|
269
|
+
* }
|
|
270
|
+
* isValid("https://example.com/"); // → true
|
|
271
|
+
* isValid("ftp://example.com/"); // → false
|
|
272
|
+
*/
|
|
86
273
|
function parse(url, opts) {
|
|
87
274
|
opts = opts || {};
|
|
88
275
|
var allowed = Array.isArray(opts.allowedProtocols) && opts.allowedProtocols.length > 0
|
|
@@ -145,6 +332,51 @@ function parse(url, opts) {
|
|
|
145
332
|
"reads at call time), or pass opts.allowUserinfo: true to opt this URL in.");
|
|
146
333
|
}
|
|
147
334
|
|
|
335
|
+
// IDN homograph defense — each host label MUST be single-script
|
|
336
|
+
// (UTS #39 §5). A label that mixes Cyrillic + Latin (e.g. `gооgle.com`
|
|
337
|
+
// with Cyrillic 'о' inside the otherwise-Latin label) presents
|
|
338
|
+
// visually as a trusted host while resolving via DNS to attacker-
|
|
339
|
+
// controlled infrastructure. Defaults to refuse; operators with
|
|
340
|
+
// legitimate non-Latin host labels opt in via `allowMixedScript: true`
|
|
341
|
+
// and the opt-in audits with the host so a forensic review can
|
|
342
|
+
// reconstruct which call sites accept mixed-script hosts. Per-label
|
|
343
|
+
// detection (not whole-host) so a legitimate `eu.shop.example.org`
|
|
344
|
+
// mixing Latin + Cyrillic across labels still refuses. Node's URL
|
|
345
|
+
// parser normalizes IDN hosts to Punycode (`xn--`), so we decode each
|
|
346
|
+
// label to Unicode first via nodeUrl.domainToUnicode and run the
|
|
347
|
+
// mixed-script catalog on the decoded codepoints.
|
|
348
|
+
if (opts.allowMixedScript !== true && parsed.hostname) {
|
|
349
|
+
var unicodeHost;
|
|
350
|
+
try { unicodeHost = nodeUrl.domainToUnicode(parsed.hostname); }
|
|
351
|
+
catch (_e) { unicodeHost = parsed.hostname; }
|
|
352
|
+
var labels = (unicodeHost || parsed.hostname).split(".");
|
|
353
|
+
var allowedScripts = Array.isArray(opts.allowedScripts) ? opts.allowedScripts : null;
|
|
354
|
+
for (var li = 0; li < labels.length; li += 1) {
|
|
355
|
+
var label = labels[li];
|
|
356
|
+
if (label.length === 0) continue;
|
|
357
|
+
var mixed = codepointClass.detectMixedScripts(label, allowedScripts);
|
|
358
|
+
if (mixed) {
|
|
359
|
+
try {
|
|
360
|
+
audit().safeEmit({
|
|
361
|
+
action: "safeurl.idn_homograph.refused",
|
|
362
|
+
outcome: "denied",
|
|
363
|
+
metadata: {
|
|
364
|
+
host: parsed.hostname,
|
|
365
|
+
label: label,
|
|
366
|
+
scripts: mixed,
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
} catch (_e) { /* audit best-effort */ }
|
|
370
|
+
throw _makeError(errClass, "safe-url/idn-homograph",
|
|
371
|
+
"URL host label '" + label + "' mixes scripts (" + mixed.join(", ") +
|
|
372
|
+
") — IDN homograph attack shape (UTS #39 §5). Refuses by default; " +
|
|
373
|
+
"operators with a legitimate mixed-script host pass " +
|
|
374
|
+
"opts.allowMixedScript: true (with an audited reason) or " +
|
|
375
|
+
"opts.allowedScripts: ['latin','cyrillic'] to allowlist specific scripts.");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
148
380
|
return parsed;
|
|
149
381
|
}
|
|
150
382
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* sandbox-worker — bootstrap module loaded inside the worker_threads
|
|
4
|
+
* Worker spawned by lib/sandbox.js. Runs UNTRUSTED operator-supplied
|
|
5
|
+
* source against a pre-stripped global scope.
|
|
6
|
+
*
|
|
7
|
+
* NOT operator-facing — operators interact via b.sandbox.run().
|
|
8
|
+
*
|
|
9
|
+
* Wire format:
|
|
10
|
+
* workerData: {
|
|
11
|
+
* source: string, // operator-supplied JS — function body
|
|
12
|
+
* input: any, // pass-through input
|
|
13
|
+
* allowedGlobals: string[], // intersected with KNOWN_SAFE_BUILTINS
|
|
14
|
+
* maxResultBytes: number, // hard-cap on JSON.stringify(result)
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* Posts back via parentPort:
|
|
18
|
+
* { ok: true, resultJson, runtimeMs, peakBytes }
|
|
19
|
+
* { ok: false, code, message, runtimeMs, peakBytes }
|
|
20
|
+
*
|
|
21
|
+
* Containment summary:
|
|
22
|
+
* - require / process / Buffer / setTimeout / setInterval / setImmediate /
|
|
23
|
+
* queueMicrotask / global are deleted off globalThis before the
|
|
24
|
+
* operator code is compiled.
|
|
25
|
+
* - The operator source is compiled via the JS language's
|
|
26
|
+
* string-to-callable primitive — the compiled function's outer
|
|
27
|
+
* scope is GLOBAL (stripped) and CANNOT see the bootstrap's
|
|
28
|
+
* own locals (require, workerThreads, parentPort).
|
|
29
|
+
* - Resource limits (maxOldGenerationSizeMb / maxYoungGenerationSizeMb /
|
|
30
|
+
* codeRangeSizeMb / stackSizeMb) are set by the host on Worker
|
|
31
|
+
* construction; v8 kills the worker on heap overflow.
|
|
32
|
+
* - Output is JSON-serialized; the worker refuses any result whose
|
|
33
|
+
* stringified form exceeds maxResultBytes.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
var workerThreads = require("node:worker_threads");
|
|
37
|
+
|
|
38
|
+
(function () {
|
|
39
|
+
var data = workerThreads.workerData || {};
|
|
40
|
+
var allowed = Array.isArray(data.allowedGlobals) ? data.allowedGlobals : [];
|
|
41
|
+
var maxResultBytes = (typeof data.maxResultBytes === "number") ? data.maxResultBytes : null;
|
|
42
|
+
|
|
43
|
+
var ALWAYS_AVAILABLE = [
|
|
44
|
+
"Object", "Array", "String", "Number", "Boolean", "Symbol",
|
|
45
|
+
"Promise", "Error", "TypeError", "RangeError", "RegExp",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
var keep = Object.create(null);
|
|
49
|
+
for (var i = 0; i < ALWAYS_AVAILABLE.length; i += 1) keep[ALWAYS_AVAILABLE[i]] = true;
|
|
50
|
+
for (var j = 0; j < allowed.length; j += 1) keep[allowed[j]] = true;
|
|
51
|
+
|
|
52
|
+
var NODE_BUILTINS = [
|
|
53
|
+
"process", "Buffer",
|
|
54
|
+
"setImmediate", "clearImmediate",
|
|
55
|
+
"setTimeout", "clearTimeout",
|
|
56
|
+
"setInterval", "clearInterval",
|
|
57
|
+
"queueMicrotask",
|
|
58
|
+
"global",
|
|
59
|
+
];
|
|
60
|
+
for (var k = 0; k < NODE_BUILTINS.length; k += 1) {
|
|
61
|
+
var nm = NODE_BUILTINS[k];
|
|
62
|
+
if (!keep[nm]) {
|
|
63
|
+
try { delete globalThis[nm]; }
|
|
64
|
+
catch (_e1) { try { globalThis[nm] = undefined; } catch (_e2) { /* best-effort */ } }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try { delete globalThis.require; } catch (_e) { /* best-effort */ }
|
|
69
|
+
|
|
70
|
+
var startedAt = Date.now();
|
|
71
|
+
var peakBytes = 0;
|
|
72
|
+
|
|
73
|
+
function snapshotPeak() {
|
|
74
|
+
try {
|
|
75
|
+
var proc = (typeof process !== "undefined") ? process : null;
|
|
76
|
+
if (proc && typeof proc.memoryUsage === "function") {
|
|
77
|
+
var u = proc.memoryUsage();
|
|
78
|
+
if (u && typeof u.heapUsed === "number" && u.heapUsed > peakBytes) peakBytes = u.heapUsed;
|
|
79
|
+
}
|
|
80
|
+
} catch (_e) { /* process gone or stripped */ }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
snapshotPeak();
|
|
84
|
+
|
|
85
|
+
// Compile operator source via the JS language's string-to-callable
|
|
86
|
+
// primitive. The compiled function's outer scope is GLOBAL (already
|
|
87
|
+
// stripped above); it cannot see this bootstrap's own locals.
|
|
88
|
+
var Compiler = (function () { return Function; }());
|
|
89
|
+
|
|
90
|
+
var fn;
|
|
91
|
+
try {
|
|
92
|
+
fn = new Compiler("input", data.source);
|
|
93
|
+
} catch (eParse) {
|
|
94
|
+
workerThreads.parentPort.postMessage({
|
|
95
|
+
ok: false, code: "sandbox/parse-error",
|
|
96
|
+
message: "sandbox source did not parse: " + (eParse && eParse.message),
|
|
97
|
+
runtimeMs: Date.now() - startedAt, peakBytes: peakBytes,
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
var result = fn(data.input);
|
|
104
|
+
snapshotPeak();
|
|
105
|
+
var runtimeMs = Date.now() - startedAt;
|
|
106
|
+
var serialized;
|
|
107
|
+
try { serialized = (result === undefined) ? undefined : JSON.stringify(result); }
|
|
108
|
+
catch (eSer) {
|
|
109
|
+
workerThreads.parentPort.postMessage({
|
|
110
|
+
ok: false, code: "sandbox/result-not-serializable",
|
|
111
|
+
message: "sandbox result is not JSON-serializable: " + (eSer && eSer.message),
|
|
112
|
+
runtimeMs: runtimeMs, peakBytes: peakBytes,
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (maxResultBytes !== null && serialized && serialized.length > maxResultBytes) {
|
|
117
|
+
workerThreads.parentPort.postMessage({
|
|
118
|
+
ok: false, code: "sandbox/oversized-result",
|
|
119
|
+
message: "sandbox result exceeded maxResultBytes (" + serialized.length + " > " + maxResultBytes + ")",
|
|
120
|
+
runtimeMs: runtimeMs, peakBytes: peakBytes,
|
|
121
|
+
});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
workerThreads.parentPort.postMessage({
|
|
125
|
+
ok: true, resultJson: serialized, runtimeMs: runtimeMs, peakBytes: peakBytes,
|
|
126
|
+
});
|
|
127
|
+
} catch (eRun) {
|
|
128
|
+
snapshotPeak();
|
|
129
|
+
workerThreads.parentPort.postMessage({
|
|
130
|
+
ok: false, code: "sandbox/runtime-error",
|
|
131
|
+
message: "sandbox transform threw: " + (eRun && eRun.message ? eRun.message : String(eRun)),
|
|
132
|
+
runtimeMs: Date.now() - startedAt, peakBytes: peakBytes,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}());
|