@blamejs/core 0.8.43 → 0.8.50
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/ssrf-guard.js
CHANGED
|
@@ -1,43 +1,38 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* b.httpClient
|
|
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
|
-
* The validated IPs are returned in the result and `b.httpClient` pins
|
|
37
|
-
* the actual TCP connect to those exact addresses (via a custom
|
|
38
|
-
* `lookup` callback passed to https / http2 connect). A hostile DNS
|
|
39
|
-
* server cannot rebind between guard-check and connect to redirect
|
|
40
|
-
* traffic at a private / metadata address.
|
|
3
|
+
* @module b.ssrfGuard
|
|
4
|
+
* @nav HTTP
|
|
5
|
+
* @title SSRF Guard
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Outbound-URL Server-Side-Request-Forgery defense. Every URL the
|
|
9
|
+
* framework dials on behalf of an operator (b.httpClient, webhook
|
|
10
|
+
* delivery, OAuth discovery, OIDC JWKS fetch, image-by-URL upload)
|
|
11
|
+
* routes through the gate. The gate refuses private (RFC 1918 +
|
|
12
|
+
* RFC 4193 ULA), loopback (127/8 + ::1), link-local
|
|
13
|
+
* (169.254/16 + fe80::/10), reserved / documentation / CGNAT,
|
|
14
|
+
* IPv4-mapped / 6to4 / NAT64 / discard-prefix wrappers, and the
|
|
15
|
+
* cloud-metadata IPs (169.254.169.254 AWS/GCP/Azure/OpenStack/DO,
|
|
16
|
+
* 169.254.170.2 AWS ECS task role, fd00:ec2::254 IPv6 IMDS).
|
|
17
|
+
*
|
|
18
|
+
* DNS rebinding is closed by resolving the hostname once during
|
|
19
|
+
* classification AND returning the validated IP set in the result.
|
|
20
|
+
* b.httpClient pins the actual TCP connect to those exact addresses
|
|
21
|
+
* via a custom `lookup` callback — a hostile DNS server cannot flip
|
|
22
|
+
* the answer between guard-check and connect. Redirect chains are
|
|
23
|
+
* re-validated end-to-end by b.httpClient (each Location header
|
|
24
|
+
* passes through `checkUrl` before the next hop is dialed), and
|
|
25
|
+
* `createAllowlist` builds operator-specific egress allowlists that
|
|
26
|
+
* compose on top of the framework's hard-coded ban list.
|
|
27
|
+
*
|
|
28
|
+
* Cloud-metadata IPs are blocked unconditionally — `allowInternal`
|
|
29
|
+
* does NOT override this class because metadata endpoints leak
|
|
30
|
+
* instance credentials. Operators with a legitimate need for the
|
|
31
|
+
* metadata service do it through their cloud SDK with explicit IAM,
|
|
32
|
+
* never through the framework's outbound HTTP.
|
|
33
|
+
*
|
|
34
|
+
* @card
|
|
35
|
+
* Outbound-URL Server-Side-Request-Forgery defense.
|
|
41
36
|
*/
|
|
42
37
|
|
|
43
38
|
var dns = require("node:dns").promises;
|
|
@@ -64,6 +59,30 @@ var IPV6_BYTES = C.BYTES.bytes(16); // 128 bits / 8 bits-per-byte
|
|
|
64
59
|
var IPV6_GROUPS = C.BYTES.bytes(8); // hex groups
|
|
65
60
|
var HEX_RADIX = C.BYTES.bytes(16); // parseInt / toString radix
|
|
66
61
|
|
|
62
|
+
/**
|
|
63
|
+
* @primitive b.ssrfGuard.SsrfError
|
|
64
|
+
* @signature b.ssrfGuard.SsrfError
|
|
65
|
+
* @since 0.7.0
|
|
66
|
+
* @status stable
|
|
67
|
+
* @related b.ssrfGuard.checkUrl, b.ssrfGuard.createAllowlist
|
|
68
|
+
*
|
|
69
|
+
* Error class thrown by every `b.ssrfGuard` primitive on a refused
|
|
70
|
+
* URL or refused address. Extends `FrameworkError`. Carries a stable
|
|
71
|
+
* `.code` (e.g. `ssrf-guard/blocked-cloud-metadata`,
|
|
72
|
+
* `ssrf-guard/blocked-private`, `ssrf-guard/not-on-allowlist`) plus
|
|
73
|
+
* the offending `.url` / `.ip` / `.category` for the audit log. Is
|
|
74
|
+
* marked `.permanent = true` so retry layers do not loop on it.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* var b = require("blamejs");
|
|
78
|
+
* try {
|
|
79
|
+
* await b.ssrfGuard.checkUrl("http://169.254.169.254/latest/meta-data/");
|
|
80
|
+
* } catch (e) {
|
|
81
|
+
* e instanceof b.ssrfGuard.SsrfError; // → true
|
|
82
|
+
* e.code; // → "ssrf-guard/blocked-cloud-metadata"
|
|
83
|
+
* e.category; // → "cloud-metadata"
|
|
84
|
+
* }
|
|
85
|
+
*/
|
|
67
86
|
class SsrfError extends FrameworkError {
|
|
68
87
|
constructor(message, code, ctx) {
|
|
69
88
|
super(message, code);
|
|
@@ -261,6 +280,30 @@ function _ipv6PrefixMatch(prefixBytes, prefixLen, ipBytes) {
|
|
|
261
280
|
|
|
262
281
|
// ---- Public classification API ----
|
|
263
282
|
|
|
283
|
+
/**
|
|
284
|
+
* @primitive b.ssrfGuard.classify
|
|
285
|
+
* @signature b.ssrfGuard.classify(ip)
|
|
286
|
+
* @since 0.7.0
|
|
287
|
+
* @status stable
|
|
288
|
+
* @related b.ssrfGuard.checkUrl, b.ssrfGuard.cidrContains
|
|
289
|
+
*
|
|
290
|
+
* Synchronous IP-string classifier. Returns one of `"loopback"`,
|
|
291
|
+
* `"link-local"`, `"private"`, `"reserved"`, `"cloud-metadata"`, or
|
|
292
|
+
* `null` when the address is a routable public IP (or not a valid IP
|
|
293
|
+
* at all — non-string / malformed input returns `null` rather than
|
|
294
|
+
* throwing). Recognizes IPv4-mapped (`::ffff:a.b.c.d`), 6to4
|
|
295
|
+
* (`2002::/16`), and NAT64 (`64:ff9b::/96`) v6 wrappers and reclassifies
|
|
296
|
+
* the embedded v4 address — a 6to4-wrapped private IP returns
|
|
297
|
+
* `"private"`, never `null`.
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* var b = require("blamejs");
|
|
301
|
+
* b.ssrfGuard.classify("169.254.169.254"); // → "cloud-metadata"
|
|
302
|
+
* b.ssrfGuard.classify("10.0.0.1"); // → "private"
|
|
303
|
+
* b.ssrfGuard.classify("127.0.0.1"); // → "loopback"
|
|
304
|
+
* b.ssrfGuard.classify("8.8.8.8"); // → null
|
|
305
|
+
* b.ssrfGuard.classify("::ffff:10.0.0.1"); // → "private"
|
|
306
|
+
*/
|
|
264
307
|
function classify(ip) {
|
|
265
308
|
if (typeof ip !== "string") return null;
|
|
266
309
|
var family = net.isIP(ip);
|
|
@@ -313,6 +356,27 @@ function _bufEqual(a, b) {
|
|
|
313
356
|
return Buffer.compare(a, b) === 0;
|
|
314
357
|
}
|
|
315
358
|
|
|
359
|
+
/**
|
|
360
|
+
* @primitive b.ssrfGuard.cidrContains
|
|
361
|
+
* @signature b.ssrfGuard.cidrContains(cidr, ip)
|
|
362
|
+
* @since 0.7.0
|
|
363
|
+
* @status stable
|
|
364
|
+
* @related b.ssrfGuard.classify, b.ssrfGuard.createAllowlist
|
|
365
|
+
*
|
|
366
|
+
* Returns `true` if `ip` falls inside the CIDR block `cidr`, else
|
|
367
|
+
* `false`. Both arguments must be the same address family (v4-in-v4
|
|
368
|
+
* or v6-in-v6 — mixed families return `false`). Used internally by
|
|
369
|
+
* `checkUrl` to evaluate the operator's `allowInternal` exception
|
|
370
|
+
* list and exposed publicly so operator code can drive the same
|
|
371
|
+
* range arithmetic for routing / allowlist UI.
|
|
372
|
+
*
|
|
373
|
+
* @example
|
|
374
|
+
* var b = require("blamejs");
|
|
375
|
+
* b.ssrfGuard.cidrContains("10.0.0.0/8", "10.1.2.3"); // → true
|
|
376
|
+
* b.ssrfGuard.cidrContains("10.0.0.0/8", "11.0.0.1"); // → false
|
|
377
|
+
* b.ssrfGuard.cidrContains("fd00::/8", "fd12:3456::1"); // → true
|
|
378
|
+
* b.ssrfGuard.cidrContains("10.0.0.0/8", "::1"); // → false (mixed family)
|
|
379
|
+
*/
|
|
316
380
|
function cidrContains(cidr, ip) {
|
|
317
381
|
if (typeof cidr !== "string" || typeof ip !== "string") return false;
|
|
318
382
|
var slash = cidr.indexOf("/");
|
|
@@ -326,6 +390,63 @@ function cidrContains(cidr, ip) {
|
|
|
326
390
|
|
|
327
391
|
// ---- URL check (DNS-resolving) ----
|
|
328
392
|
|
|
393
|
+
/**
|
|
394
|
+
* @primitive b.ssrfGuard.checkUrl
|
|
395
|
+
* @signature b.ssrfGuard.checkUrl(url, opts?)
|
|
396
|
+
* @since 0.7.0
|
|
397
|
+
* @status stable
|
|
398
|
+
* @related b.ssrfGuard.classify, b.ssrfGuard.createAllowlist, b.httpClient.request
|
|
399
|
+
*
|
|
400
|
+
* Async DNS-resolving URL gate — the canonical pre-flight before
|
|
401
|
+
* any outbound fetch / webhook delivery / OAuth discovery. Resolves
|
|
402
|
+
* the hostname (via `b.network.dns` when available so DoH overrides
|
|
403
|
+
* apply, else native `dns.lookup`), classifies every returned address,
|
|
404
|
+
* and throws `SsrfError` on the first refused class. Cloud-metadata
|
|
405
|
+
* IPs throw unconditionally; other classes can be overridden by
|
|
406
|
+
* `allowInternal: true` (allow every private class) or
|
|
407
|
+
* `allowInternal: ["10.0.0.0/8", ...]` (allow specific CIDRs only).
|
|
408
|
+
*
|
|
409
|
+
* Returns `{ url, ips }` on success — `ips` is the resolved address
|
|
410
|
+
* list, suitable for passing to `https.request({ lookup })` so the
|
|
411
|
+
* subsequent TCP connect pins to the validated set and a hostile DNS
|
|
412
|
+
* server cannot rebind between guard-check and connect.
|
|
413
|
+
*
|
|
414
|
+
* @opts
|
|
415
|
+
* allowInternal: boolean | string[], // override private-range refusal
|
|
416
|
+
* // (cloud-metadata is NEVER overridable)
|
|
417
|
+
* errorClass: Function, // subclass of SsrfError to throw
|
|
418
|
+
* dnsLookup: Function, // override DNS resolver (testing / fixtures)
|
|
419
|
+
*
|
|
420
|
+
* @example
|
|
421
|
+
* // assertSafe before fetch — refuse private / metadata / loopback
|
|
422
|
+
* var b = require("blamejs");
|
|
423
|
+
* var result = await b.ssrfGuard.checkUrl("https://api.partner.example.com/v1/x");
|
|
424
|
+
* result.ips[0].address; // → "203.0.113.42"
|
|
425
|
+
*
|
|
426
|
+
* // Pin TCP connect to the validated IP set (defeats DNS rebinding):
|
|
427
|
+
* var validatedIps = result.ips;
|
|
428
|
+
* var lookup = function (host, opts, cb) { cb(null, validatedIps[0].address, validatedIps[0].family); };
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* // Allow an internal mesh CIDR for one specific call:
|
|
432
|
+
* var b = require("blamejs");
|
|
433
|
+
* await b.ssrfGuard.checkUrl("http://10.0.5.42:8080/health", {
|
|
434
|
+
* allowInternal: ["10.0.0.0/8"],
|
|
435
|
+
* });
|
|
436
|
+
* // → { url: parsedUrl, ips: [{ address: "10.0.5.42", family: 4 }] }
|
|
437
|
+
*
|
|
438
|
+
* @example
|
|
439
|
+
* // Cloud-metadata IPs are blocked unconditionally (allowInternal does NOT override):
|
|
440
|
+
* var b = require("blamejs");
|
|
441
|
+
* try {
|
|
442
|
+
* await b.ssrfGuard.checkUrl("http://169.254.169.254/latest/meta-data/iam/", {
|
|
443
|
+
* allowInternal: true,
|
|
444
|
+
* });
|
|
445
|
+
* } catch (e) {
|
|
446
|
+
* e.code; // → "ssrf-guard/blocked-cloud-metadata"
|
|
447
|
+
* e.category; // → "cloud-metadata"
|
|
448
|
+
* }
|
|
449
|
+
*/
|
|
329
450
|
async function checkUrl(url, opts) {
|
|
330
451
|
opts = opts || {};
|
|
331
452
|
validateOpts(opts, ["allowInternal", "errorClass", "dnsLookup"], "ssrfGuard.checkUrl");
|
|
@@ -403,19 +524,54 @@ async function checkUrl(url, opts) {
|
|
|
403
524
|
return { url: parsed, ips: ips };
|
|
404
525
|
}
|
|
405
526
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
527
|
+
/**
|
|
528
|
+
* @primitive b.ssrfGuard.createAllowlist
|
|
529
|
+
* @signature b.ssrfGuard.createAllowlist(opts)
|
|
530
|
+
* @since 0.7.0
|
|
531
|
+
* @status stable
|
|
532
|
+
* @related b.ssrfGuard.checkUrl, b.ssrfGuard.cidrContains
|
|
533
|
+
*
|
|
534
|
+
* Build a contextual per-call egress allowlist composing on top of
|
|
535
|
+
* `ssrfGuard`. Operators describe an allowed host / CIDR set plus an
|
|
536
|
+
* optional denylist; the returned `{ assert(url) }` either resolves
|
|
537
|
+
* to the validated IP set (delegating to `checkUrl` with
|
|
538
|
+
* `allowInternal: true` because the explicit allowlist supersedes
|
|
539
|
+
* the private-range refusal) or throws `SsrfError`. Distinct from
|
|
540
|
+
* `checkUrl`'s hard-coded ban list — use `createAllowlist` when the
|
|
541
|
+
* deployment has SPECIFIC outbound targets and everything else
|
|
542
|
+
* should be refused.
|
|
543
|
+
*
|
|
544
|
+
* Throws at construction time if `allow` is empty (an empty
|
|
545
|
+
* allowlist would refuse every URL — almost certainly a config typo).
|
|
546
|
+
*
|
|
547
|
+
* @opts
|
|
548
|
+
* allow: string[], // required; entries are exact hostnames OR CIDR blocks
|
|
549
|
+
* deny: string[], // optional; checked AFTER allow — denylist wins
|
|
550
|
+
*
|
|
551
|
+
* @example
|
|
552
|
+
* // Allow-list a single partner domain — refuse everything else:
|
|
553
|
+
* var b = require("blamejs");
|
|
554
|
+
* var egress = b.ssrfGuard.createAllowlist({
|
|
555
|
+
* allow: ["api.partner.example.com", "203.0.113.0/24"],
|
|
556
|
+
* deny: ["evil.partner.example.com"],
|
|
557
|
+
* });
|
|
558
|
+
* await egress.assert("https://api.partner.example.com/v1/x");
|
|
559
|
+
* // → { url: parsedUrl, ips: [{ address: "203.0.113.10", family: 4 }] }
|
|
560
|
+
*
|
|
561
|
+
* @example
|
|
562
|
+
* // Custom blocklist for cloud-metadata IPs at the allowlist layer
|
|
563
|
+
* // (defense-in-depth; checkUrl already refuses these unconditionally):
|
|
564
|
+
* var b = require("blamejs");
|
|
565
|
+
* var egress = b.ssrfGuard.createAllowlist({
|
|
566
|
+
* allow: ["10.0.0.0/8"],
|
|
567
|
+
* deny: ["169.254.169.254", "169.254.170.2"],
|
|
568
|
+
* });
|
|
569
|
+
* try {
|
|
570
|
+
* await egress.assert("http://169.254.169.254/latest/");
|
|
571
|
+
* } catch (e) {
|
|
572
|
+
* e.code; // → "ssrf-guard/blocked-cloud-metadata"
|
|
573
|
+
* }
|
|
574
|
+
*/
|
|
419
575
|
function createAllowlist(opts) {
|
|
420
576
|
opts = opts || {};
|
|
421
577
|
var allowList = Array.isArray(opts.allow) ? opts.allow.slice() : [];
|
|
@@ -457,15 +613,119 @@ function createAllowlist(opts) {
|
|
|
457
613
|
return { assert: assertUrl };
|
|
458
614
|
}
|
|
459
615
|
|
|
616
|
+
/**
|
|
617
|
+
* @primitive b.ssrfGuard.isPrivate
|
|
618
|
+
* @signature b.ssrfGuard.isPrivate(ip)
|
|
619
|
+
* @since 0.7.0
|
|
620
|
+
* @status stable
|
|
621
|
+
* @related b.ssrfGuard.classify
|
|
622
|
+
*
|
|
623
|
+
* Returns `true` if `ip` is in an RFC 1918 IPv4 private range
|
|
624
|
+
* (10/8, 172.16/12, 192.168/16) or RFC 4193 IPv6 ULA (fc00::/7).
|
|
625
|
+
* Convenience wrapper over `classify(ip) === "private"`.
|
|
626
|
+
*
|
|
627
|
+
* @example
|
|
628
|
+
* var b = require("blamejs");
|
|
629
|
+
* b.ssrfGuard.isPrivate("10.0.0.1"); // → true
|
|
630
|
+
* b.ssrfGuard.isPrivate("8.8.8.8"); // → false
|
|
631
|
+
* b.ssrfGuard.isPrivate("fd12:3456::1"); // → true
|
|
632
|
+
*/
|
|
633
|
+
function isPrivate(ip) { return classify(ip) === "private"; }
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* @primitive b.ssrfGuard.isLoopback
|
|
637
|
+
* @signature b.ssrfGuard.isLoopback(ip)
|
|
638
|
+
* @since 0.7.0
|
|
639
|
+
* @status stable
|
|
640
|
+
* @related b.ssrfGuard.classify
|
|
641
|
+
*
|
|
642
|
+
* Returns `true` if `ip` is in 127/8 (IPv4 loopback) or `::1`
|
|
643
|
+
* (IPv6 loopback). Convenience wrapper over
|
|
644
|
+
* `classify(ip) === "loopback"`.
|
|
645
|
+
*
|
|
646
|
+
* @example
|
|
647
|
+
* var b = require("blamejs");
|
|
648
|
+
* b.ssrfGuard.isLoopback("127.0.0.1"); // → true
|
|
649
|
+
* b.ssrfGuard.isLoopback("::1"); // → true
|
|
650
|
+
* b.ssrfGuard.isLoopback("8.8.8.8"); // → false
|
|
651
|
+
*/
|
|
652
|
+
function isLoopback(ip) { return classify(ip) === "loopback"; }
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* @primitive b.ssrfGuard.isLinkLocal
|
|
656
|
+
* @signature b.ssrfGuard.isLinkLocal(ip)
|
|
657
|
+
* @since 0.7.0
|
|
658
|
+
* @status stable
|
|
659
|
+
* @related b.ssrfGuard.classify, b.ssrfGuard.isCloudMetadata
|
|
660
|
+
*
|
|
661
|
+
* Returns `true` if `ip` is in 169.254/16 (IPv4 link-local) or
|
|
662
|
+
* fe80::/10 (IPv6 link-local). Note that the cloud-metadata IPs
|
|
663
|
+
* (169.254.169.254 / 169.254.170.2) classify as `"cloud-metadata"`,
|
|
664
|
+
* NOT `"link-local"` — use `isCloudMetadata` if that distinction
|
|
665
|
+
* matters.
|
|
666
|
+
*
|
|
667
|
+
* @example
|
|
668
|
+
* var b = require("blamejs");
|
|
669
|
+
* b.ssrfGuard.isLinkLocal("169.254.0.1"); // → true
|
|
670
|
+
* b.ssrfGuard.isLinkLocal("169.254.169.254"); // → false (it's cloud-metadata)
|
|
671
|
+
* b.ssrfGuard.isLinkLocal("fe80::1"); // → true
|
|
672
|
+
*/
|
|
673
|
+
function isLinkLocal(ip) { return classify(ip) === "link-local"; }
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* @primitive b.ssrfGuard.isCloudMetadata
|
|
677
|
+
* @signature b.ssrfGuard.isCloudMetadata(ip)
|
|
678
|
+
* @since 0.7.0
|
|
679
|
+
* @status stable
|
|
680
|
+
* @related b.ssrfGuard.classify, b.ssrfGuard.checkUrl
|
|
681
|
+
*
|
|
682
|
+
* Returns `true` if `ip` is one of the cloud-metadata service
|
|
683
|
+
* addresses (169.254.169.254 AWS/GCP/Azure/OpenStack/DO,
|
|
684
|
+
* 169.254.170.2 AWS ECS task role, fd00:ec2::254 IPv6 IMDS).
|
|
685
|
+
* These IPs leak instance credentials and `checkUrl` refuses them
|
|
686
|
+
* unconditionally — `allowInternal` does NOT override.
|
|
687
|
+
*
|
|
688
|
+
* @example
|
|
689
|
+
* var b = require("blamejs");
|
|
690
|
+
* b.ssrfGuard.isCloudMetadata("169.254.169.254"); // → true
|
|
691
|
+
* b.ssrfGuard.isCloudMetadata("169.254.170.2"); // → true
|
|
692
|
+
* b.ssrfGuard.isCloudMetadata("fd00:ec2::254"); // → true
|
|
693
|
+
* b.ssrfGuard.isCloudMetadata("169.254.0.1"); // → false (link-local but not metadata)
|
|
694
|
+
*/
|
|
695
|
+
function isCloudMetadata(ip) { return classify(ip) === "cloud-metadata"; }
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* @primitive b.ssrfGuard.isReserved
|
|
699
|
+
* @signature b.ssrfGuard.isReserved(ip)
|
|
700
|
+
* @since 0.7.0
|
|
701
|
+
* @status stable
|
|
702
|
+
* @related b.ssrfGuard.classify
|
|
703
|
+
*
|
|
704
|
+
* Returns `true` if `ip` is in an IETF-reserved range — 0/8 ("this
|
|
705
|
+
* network"), 100.64/10 (CGNAT, RFC 6598), 192.0.0/24 (IETF protocol
|
|
706
|
+
* assignments), TEST-NET-1/2/3 (192.0.2/24, 198.51.100/24, 203.0.113/24),
|
|
707
|
+
* 198.18/15 (network benchmark), 224/4 (multicast), 240/4 (reserved +
|
|
708
|
+
* 255.255.255.255), 2001:db8::/32 (IPv6 documentation), ff00::/8 (IPv6
|
|
709
|
+
* multicast), or 100::/64 (IPv6 discard prefix).
|
|
710
|
+
*
|
|
711
|
+
* @example
|
|
712
|
+
* var b = require("blamejs");
|
|
713
|
+
* b.ssrfGuard.isReserved("192.0.2.1"); // → true (TEST-NET-1)
|
|
714
|
+
* b.ssrfGuard.isReserved("100.64.0.1"); // → true (CGNAT)
|
|
715
|
+
* b.ssrfGuard.isReserved("224.0.0.1"); // → true (multicast)
|
|
716
|
+
* b.ssrfGuard.isReserved("8.8.8.8"); // → false
|
|
717
|
+
*/
|
|
718
|
+
function isReserved(ip) { return classify(ip) === "reserved"; }
|
|
719
|
+
|
|
460
720
|
module.exports = {
|
|
461
721
|
classify: classify,
|
|
462
722
|
cidrContains: cidrContains,
|
|
463
723
|
checkUrl: checkUrl,
|
|
464
724
|
createAllowlist: createAllowlist,
|
|
465
|
-
isPrivate:
|
|
466
|
-
isLoopback:
|
|
467
|
-
isLinkLocal:
|
|
468
|
-
isCloudMetadata:
|
|
469
|
-
isReserved:
|
|
725
|
+
isPrivate: isPrivate,
|
|
726
|
+
isLoopback: isLoopback,
|
|
727
|
+
isLinkLocal: isLinkLocal,
|
|
728
|
+
isCloudMetadata: isCloudMetadata,
|
|
729
|
+
isReserved: isReserved,
|
|
470
730
|
SsrfError: SsrfError,
|
|
471
731
|
};
|