SSRF Guard

Outbound-URL Server-Side-Request-Forgery defense. Every URL the framework dials on behalf of an operator (b.httpClient, webhook delivery, OAuth discovery, OIDC JWKS fetch, image-by-URL upload) routes through the gate. The gate refuses private (RFC 1918 + RFC 4193 ULA), loopback (127/8 + ::1), link-local (169.254/16 + fe80::/10), reserved / documentation / CGNAT, IPv4-mapped / 6to4 / NAT64 / discard-prefix wrappers, and the cloud-metadata IPs (169.254.169.254 AWS/GCP/Azure/OpenStack/DO, 169.254.170.2 AWS ECS task role, fd00:ec2::254 IPv6 IMDS).

DNS rebinding is closed by resolving the hostname once during classification AND returning the validated IP set in the result. b.httpClient pins the actual TCP connect to those exact addresses via a custom lookup callback — a hostile DNS server cannot flip the answer between guard-check and connect. Redirect chains are re-validated end-to-end by b.httpClient (each Location header passes through checkUrl before the next hop is dialed), and createAllowlist builds operator-specific egress allowlists that compose on top of the framework's hard-coded ban list.

Cloud-metadata IPs are blocked unconditionally — allowInternal does NOT override this class because metadata endpoints leak instance credentials. Operators with a legitimate need for the metadata service do it through their cloud SDK with explicit IAM, never through the framework's outbound HTTP.

b.ssrfGuard.SsrfError #

stable0.7.0

Error class thrown by every b.ssrfGuard primitive on a refused URL or refused address. Extends FrameworkError. Carries a stable .code (e.g. ssrf-guard/blocked-cloud-metadata, ssrf-guard/blocked-private, ssrf-guard/not-on-allowlist) plus the offending .url / .ip / .category for the audit log. Is marked .permanent = true so retry layers do not loop on it.

var b = require("blamejs");
try {
  await b.ssrfGuard.checkUrl("http://169.254.169.254/latest/meta-data/");
} catch (e) {
  e instanceof b.ssrfGuard.SsrfError;   // → true
  e.code;                               // → "ssrf-guard/blocked-cloud-metadata"
  e.category;                           // → "cloud-metadata"
}

b.ssrfGuard.canonicalizeHost(host) #

stable0.15.6

Canonicalize a bare host string to its single comparable form for host allowlists, dedup keys, and SSRF pre-checks. A net.isIP-valid IP literal collapses to one canonical string: a dotted-quad IPv4 stays dotted-quad; IPv6 in any zero-compression / mixed-case / IPv4-mapped spelling ([0:0:0:0:0:ffff:7f00:1], ::FFFF:7F00:1) becomes the RFC 5952 lower-hex compressed form. The IP bytes are parsed by the SAME routines classify matches on, so the canonical string and the SSRF verdict can never disagree about which address a host is.

The numeric-base IPv4 decode (octal 0177.0.0.1, hex 0x7f000001, single-integer 2130706433, shorthand 127.1) is the WHATWG URL parser's job — b.safeUrl.canonicalize runs that FIRST and hands this primitive the already-decoded dotted-quad. This is the IP-byte + case layer, not the base decoder.

A DNS name (not an IP literal) is lowercased and any trailing dot is stripped — Example.COM.example.com. IDN A-label / U-label normalization is NOT done here (the WHATWG URL parser owns that via b.safeUrl.canonicalize). [...]-bracketed IPv6 input is accepted (brackets stripped); the returned IPv6 string is UNbracketed (the URL layer re-adds brackets).

var b = require("blamejs");
b.ssrfGuard.canonicalizeHost("[0:0:0:0:0:0:0:1]");    // → "::1"
b.ssrfGuard.canonicalizeHost("::FFFF:7F00:1");        // → "::ffff:7f00:1"
b.ssrfGuard.canonicalizeHost("Example.COM.");         // → "example.com"

b.ssrfGuard.classify(ip) #

stable0.7.0

Synchronous IP-string classifier. Returns one of "loopback", "link-local", "private", "reserved", "cloud-metadata", or null when the address is a routable public IP (or not a valid IP at all — non-string / malformed input returns null rather than throwing). Recognizes IPv4-mapped (::ffff:a.b.c.d), 6to4 (2002::/16), and NAT64 (64:ff9b::/96) v6 wrappers and reclassifies the embedded v4 address — a 6to4-wrapped private IP returns "private", never null.

var b = require("blamejs");
b.ssrfGuard.classify("169.254.169.254");   // → "cloud-metadata"
b.ssrfGuard.classify("10.0.0.1");          // → "private"
b.ssrfGuard.classify("127.0.0.1");         // → "loopback"
b.ssrfGuard.classify("8.8.8.8");           // → null
b.ssrfGuard.classify("::ffff:10.0.0.1");   // → "private"

b.ssrfGuard.cidrContains(cidr, ip) #

stable0.7.0

Returns true if ip falls inside the CIDR block cidr, else false. Both arguments must be the same address family (v4-in-v4 or v6-in-v6 — mixed families return false). Used internally by checkUrl to evaluate the operator's allowInternal exception list and exposed publicly so operator code can drive the same range arithmetic for routing / allowlist UI.

var b = require("blamejs");
b.ssrfGuard.cidrContains("10.0.0.0/8",   "10.1.2.3");      // → true
b.ssrfGuard.cidrContains("10.0.0.0/8",   "11.0.0.1");      // → false
b.ssrfGuard.cidrContains("fd00::/8",     "fd12:3456::1");  // → true
b.ssrfGuard.cidrContains("10.0.0.0/8",   "::1");           // → false (mixed family)

b.ssrfGuard.checkUrl(url, opts?) #

stable0.7.0
{
  allowInternal: boolean | string[],   // override private-range refusal
                                       //   (cloud-metadata is NEVER overridable)
  errorClass:    Function,             // subclass of SsrfError to throw
  dnsLookup:     Function,             // override DNS resolver (testing / fixtures)
}

Async DNS-resolving URL gate — the canonical pre-flight before any outbound fetch / webhook delivery / OAuth discovery. Resolves the hostname (via b.network.dns when available so DoH overrides apply, else native dns.lookup), classifies every returned address, and throws SsrfError on the first refused class. Cloud-metadata IPs throw unconditionally; other classes can be overridden by allowInternal: true (allow every private class) or allowInternal: ["10.0.0.0/8", ...] (allow specific CIDRs only).

Returns { url, ips } on success — ips is the resolved address list, suitable for passing to https.request({ lookup }) so the subsequent TCP connect pins to the validated set and a hostile DNS server cannot rebind between guard-check and connect.

// assertSafe before fetch — refuse private / metadata / loopback
var b = require("blamejs");
var result = await b.ssrfGuard.checkUrl("https://api.partner.example.com/v1/x");
result.ips[0].address;   // → "203.0.113.42"

// Pin TCP connect to the validated IP set (defeats DNS rebinding):
var validatedIps = result.ips;
var lookup = function (host, opts, cb) { cb(null, validatedIps[0].address, validatedIps[0].family); };

// Allow an internal mesh CIDR for one specific call:
var b = require("blamejs");
await b.ssrfGuard.checkUrl("http://10.0.5.42:8080/health", {
  allowInternal: ["10.0.0.0/8"],
});
// → { url: parsedUrl, ips: [{ address: "10.0.5.42", family: 4 }] }

// Cloud-metadata IPs are blocked unconditionally (allowInternal does NOT override):
var b = require("blamejs");
try {
  await b.ssrfGuard.checkUrl("http://169.254.169.254/latest/meta-data/iam/", {
    allowInternal: true,
  });
} catch (e) {
  e.code;       // → "ssrf-guard/blocked-cloud-metadata"
  e.category;   // → "cloud-metadata"
}

b.ssrfGuard.createAllowlist(opts) #

stable0.7.0
{
  allow: string[],   // required; entries are exact hostnames OR CIDR blocks
  deny:  string[],   // optional; checked AFTER allow — denylist wins
}

Build a contextual per-call egress allowlist composing on top of ssrfGuard. Operators describe an allowed host / CIDR set plus an optional denylist; the returned { assert(url) } either resolves to the validated IP set (delegating to checkUrl with allowInternal: true because the explicit allowlist supersedes the private-range refusal) or throws SsrfError. Distinct from checkUrl's hard-coded ban list — use createAllowlist when the deployment has SPECIFIC outbound targets and everything else should be refused.

Throws at construction time if allow is empty (an empty allowlist would refuse every URL — almost certainly a config typo).

// Allow-list a single partner domain — refuse everything else:
var b = require("blamejs");
var egress = b.ssrfGuard.createAllowlist({
  allow: ["api.partner.example.com", "203.0.113.0/24"],
  deny:  ["evil.partner.example.com"],
});
await egress.assert("https://api.partner.example.com/v1/x");
// → { url: parsedUrl, ips: [{ address: "203.0.113.10", family: 4 }] }

// Custom blocklist for cloud-metadata IPs at the allowlist layer
// (defense-in-depth; checkUrl already refuses these unconditionally):
var b = require("blamejs");
var egress = b.ssrfGuard.createAllowlist({
  allow: ["10.0.0.0/8"],
  deny:  ["169.254.169.254", "169.254.170.2"],
});
try {
  await egress.assert("http://169.254.169.254/latest/");
} catch (e) {
  e.code;   // → "ssrf-guard/blocked-cloud-metadata"
}

b.ssrfGuard.isPrivate(ip) #

stable0.7.0

Returns true if ip is in an RFC 1918 IPv4 private range (10/8, 172.16/12, 192.168/16) or RFC 4193 IPv6 ULA (fc00::/7). Convenience wrapper over classify(ip) === "private".

var b = require("blamejs");
b.ssrfGuard.isPrivate("10.0.0.1");        // → true
b.ssrfGuard.isPrivate("8.8.8.8");         // → false
b.ssrfGuard.isPrivate("fd12:3456::1");    // → true

b.ssrfGuard.isLoopback(ip) #

stable0.7.0

Returns true if ip is in 127/8 (IPv4 loopback) or ::1 (IPv6 loopback). Convenience wrapper over classify(ip) === "loopback".

var b = require("blamejs");
b.ssrfGuard.isLoopback("127.0.0.1");   // → true
b.ssrfGuard.isLoopback("::1");         // → true
b.ssrfGuard.isLoopback("8.8.8.8");     // → false

b.ssrfGuard.isLoopbackHost(host) #

stable0.17.13

Hostname-aware loopback test. Unlike isLoopback (IP literals only) this canonicalizes host (strips [] brackets + trailing dots, folds an IPv4-mapped IPv6, lowercases) and returns true for a loopback IP literal OR the RFC 6761 §6.3 reserved names localhost and any *.localhost. Pure name-shape — never resolves DNS.

b.ssrfGuard.isLoopbackHost("localhost");     // → true
b.ssrfGuard.isLoopbackHost("[::1]");          // → true
b.ssrfGuard.isLoopbackHost("app.localhost");  // → true
b.ssrfGuard.isLoopbackHost("example.com");    // → false

b.ssrfGuard.isExactLoopbackName(host) #

stable0.17.13

Like isLoopbackHost but WITHOUT the *.localhost subdomain reservation — accepts only the exact localhost name or a loopback IP literal. For the OAuth/OIDC cleartext-redirect loopback exception (RFC 9700 §4.1.1), which must not widen to a *.localhost name an attacker could register in a resolver.

b.ssrfGuard.isExactLoopbackName("localhost");     // → true
b.ssrfGuard.isExactLoopbackName("127.0.0.1");     // → true
b.ssrfGuard.isExactLoopbackName("app.localhost"); // → false

b.ssrfGuard.isLinkLocal(ip) #

stable0.7.0

Returns true if ip is in 169.254/16 (IPv4 link-local) or fe80::/10 (IPv6 link-local). Note that the cloud-metadata IPs (169.254.169.254 / 169.254.170.2) classify as "cloud-metadata", NOT "link-local" — use isCloudMetadata if that distinction matters.

var b = require("blamejs");
b.ssrfGuard.isLinkLocal("169.254.0.1");        // → true
b.ssrfGuard.isLinkLocal("169.254.169.254");    // → false (it's cloud-metadata)
b.ssrfGuard.isLinkLocal("fe80::1");            // → true

b.ssrfGuard.isCloudMetadata(ip) #

stable0.7.0

Returns true if ip is one of the cloud-metadata service addresses (169.254.169.254 AWS/GCP/Azure/OpenStack/DO, 169.254.170.2 AWS ECS task role, fd00:ec2::254 IPv6 IMDS). These IPs leak instance credentials and checkUrl refuses them unconditionally — allowInternal does NOT override.

var b = require("blamejs");
b.ssrfGuard.isCloudMetadata("169.254.169.254");   // → true
b.ssrfGuard.isCloudMetadata("169.254.170.2");     // → true
b.ssrfGuard.isCloudMetadata("fd00:ec2::254");     // → true
b.ssrfGuard.isCloudMetadata("169.254.0.1");       // → false (link-local but not metadata)

b.ssrfGuard.isReserved(ip) #

stable0.7.0

Returns true if ip is in an IETF-reserved range — 0/8 ("this network"), 100.64/10 (CGNAT, RFC 6598), 192.0.0/24 (IETF protocol assignments), TEST-NET-1/2/3 (192.0.2/24, 198.51.100/24, 203.0.113/24), 198.18/15 (network benchmark), 224/4 (multicast), 240/4 (reserved + 255.255.255.255), 2001:db8::/32 (IPv6 documentation), ff00::/8 (IPv6 multicast), or 100::/64 (IPv6 discard prefix).

var b = require("blamejs");
b.ssrfGuard.isReserved("192.0.2.1");      // → true (TEST-NET-1)
b.ssrfGuard.isReserved("100.64.0.1");     // → true (CGNAT)
b.ssrfGuard.isReserved("224.0.0.1");      // → true (multicast)
b.ssrfGuard.isReserved("8.8.8.8");        // → false

b.ssrfGuard.checkUrlTextual(url, opts?) #

stable0.11.1
{
  errorClass?: typeof FrameworkError,    // operator-supplied error class for typed refusal
}

Text-only SSRF check for paths where the DNS lookup is intentionally deferred to a downstream resolver (e.g. an outbound HTTP proxy resolving hostnames in its own network context, or a pinned-IP transport that already knows the destination address). The hostname is checked verbatim against the cloud-metadata IP list — those addresses (169.254.169.254, 169.254.170.2, fd00:ec2::254) are NEVER overridable, even when allowInternal: true and a proxy is configured. Operators short- circuiting the DNS-resolution portion of checkUrl MUST still call this primitive so the unconditional metadata-IP block applies at the textual layer.

Returns { ips: null, host } on accept. Throws SsrfError with code: "ssrf-guard/blocked-cloud-metadata" when the hostname is an IP literal matching a known cloud-metadata IP.

b.ssrfGuard.checkUrlTextual("http://intranet-app/api");
// → { ips: null, host: "intranet-app" }

try { b.ssrfGuard.checkUrlTextual("http://169.254.169.254/x"); }
catch (e) { e.code; }
// → "ssrf-guard/blocked-cloud-metadata"

Last updated 2026-08-08T16:39:49.652Z by seeder.