@blamejs/core 0.4.2 → 0.4.4

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 CHANGED
@@ -6,6 +6,12 @@ Pre-1.0 the surface is intentionally evolving — every release may
6
6
  change something operators depend on. Read each entry before
7
7
  upgrading across more than a few patches at a time.
8
8
 
9
+ ## v0.4.x
10
+
11
+ - **0.4.2** (2026-04-30) — npm keywords
12
+ - **0.4.1** (2026-04-29) — wiki bot-guard skips /healthz so the post-publish smoke check passes
13
+ - **0.4.0** (2026-04-29) — bench suite + drops the deprecated b.logger.createLogger
14
+
9
15
  ## v0.3.x
10
16
 
11
17
  - **0.3.39** (2026-04-29) — MIGRATING.md generator scans deprecate() calls in lib/
package/index.js CHANGED
@@ -85,6 +85,7 @@ var httpClient = require("./lib/http-client");
85
85
  httpClient.encrypted = require("./lib/middleware/api-encrypt").httpClient;
86
86
  var websocket = require("./lib/websocket");
87
87
  var safeUrl = require("./lib/safe-url");
88
+ var ssrfGuard = require("./lib/ssrf-guard");
88
89
  var authHeader = require("./lib/auth-header");
89
90
  var auth = {
90
91
  password: require("./lib/auth/password"),
@@ -191,6 +192,7 @@ module.exports = {
191
192
  httpClient: httpClient,
192
193
  websocket: websocket,
193
194
  safeUrl: safeUrl,
195
+ ssrfGuard: ssrfGuard,
194
196
  authHeader: authHeader,
195
197
  auth: auth,
196
198
  template: template,
package/lib/auth/oauth.js CHANGED
@@ -268,7 +268,8 @@ function create(opts) {
268
268
  ? opts.discoveryCacheMs : DEFAULT_DISCOVERY_CACHE_MS;
269
269
  var acceptedAlgorithms = Array.isArray(opts.acceptedAlgorithms) && opts.acceptedAlgorithms.length > 0
270
270
  ? opts.acceptedAlgorithms.slice() : DEFAULT_ACCEPTED_ALGS.slice();
271
- var allowHttp = !!opts.allowHttp; // localhost dev opt-in
271
+ var allowHttp = !!opts.allowHttp; // localhost dev opt-in (scheme)
272
+ var allowInternal = opts.allowInternal != null ? opts.allowInternal : null; // localhost dev opt-in (SSRF gate)
272
273
  var httpClientOpts = opts.httpClient || {};
273
274
  var responseMode = opts.responseMode || null;
274
275
 
@@ -341,6 +342,7 @@ function create(opts) {
341
342
  method: "GET",
342
343
  }, fetchOpts);
343
344
  if (allowHttp) req.allowedProtocols = safeUrl.ALLOW_HTTP_ALL;
345
+ if (allowInternal !== null) req.allowInternal = allowInternal;
344
346
  Object.assign(req, httpClientOpts);
345
347
  var res = await hc.request(req);
346
348
  if (res.statusCode < 200 || res.statusCode >= 300) {
@@ -505,6 +507,7 @@ function create(opts) {
505
507
  body: Buffer.from(body.toString(), "utf8"),
506
508
  };
507
509
  if (allowHttp) req.allowedProtocols = safeUrl.ALLOW_HTTP_ALL;
510
+ if (allowInternal !== null) req.allowInternal = allowInternal;
508
511
  Object.assign(req, httpClientOpts);
509
512
  var res = await hc.request(req);
510
513
  // RFC 7009: 200 even if the token was already revoked / unknown.
@@ -526,6 +529,7 @@ function create(opts) {
526
529
  body: Buffer.from(body.toString(), "utf8"),
527
530
  };
528
531
  if (allowHttp) req.allowedProtocols = safeUrl.ALLOW_HTTP_ALL;
532
+ if (allowInternal !== null) req.allowInternal = allowInternal;
529
533
  Object.assign(req, httpClientOpts);
530
534
  var res = await hc.request(req);
531
535
  var text = res.body ? res.body.toString("utf8") : "";
@@ -70,6 +70,7 @@ var pqcAgent = require("./pqc-agent");
70
70
  var safeAsync = require("./safe-async");
71
71
  var safeBuffer = require("./safe-buffer");
72
72
  var safeUrl = require("./safe-url");
73
+ var ssrfGuard = require("./ssrf-guard");
73
74
  var { FrameworkError } = require("./framework-error");
74
75
 
75
76
  // Per-origin transport cache. Entry is either the resolved transport
@@ -310,18 +311,26 @@ function request(opts) {
310
311
  return Promise.reject(e);
311
312
  }
312
313
 
313
- // Caller-supplied agent bypasses transport cache (h1 only).
314
- if (opts.agent) {
315
- return _requestH1({
316
- kind: "h1",
317
- lib: u.protocol === "https:" ? https : http,
318
- agent: opts.agent,
319
- }, u, opts);
320
- }
314
+ // SSRF gate refuse private / loopback / link-local / cloud-metadata
315
+ // / reserved IP destinations by default. Operators on internal mesh
316
+ // pass `allowInternal: true` (or a CIDR list for narrower bypass).
317
+ return ssrfGuard.checkUrl(u, {
318
+ allowInternal: opts.allowInternal,
319
+ errorClass: opts.errorClass,
320
+ }).then(function () {
321
+ // Caller-supplied agent bypasses transport cache (h1 only).
322
+ if (opts.agent) {
323
+ return _requestH1({
324
+ kind: "h1",
325
+ lib: u.protocol === "https:" ? https : http,
326
+ agent: opts.agent,
327
+ }, u, opts);
328
+ }
321
329
 
322
- return _getTransport(u, opts).then(function (transport) {
323
- if (transport.kind === "h2") return _requestH2(transport, u, opts);
324
- return _requestH1(transport, u, opts);
330
+ return _getTransport(u, opts).then(function (transport) {
331
+ if (transport.kind === "h2") return _requestH2(transport, u, opts);
332
+ return _requestH1(transport, u, opts);
333
+ });
325
334
  });
326
335
  }
327
336
 
@@ -60,7 +60,7 @@ function _authHeaders(config) {
60
60
  return authHeader.fromConfig(config);
61
61
  }
62
62
 
63
- function _post(url, body, headers, timeoutMs, allowedProtocols) {
63
+ function _post(url, body, headers, timeoutMs, allowedProtocols, allowInternal) {
64
64
  return httpClient.request({
65
65
  method: "POST",
66
66
  url: url,
@@ -70,6 +70,7 @@ function _post(url, body, headers, timeoutMs, allowedProtocols) {
70
70
  maxResponseBytes: MAX_RESPONSE_BYTES,
71
71
  errorClass: LogStreamError,
72
72
  allowedProtocols: allowedProtocols,
73
+ allowInternal: allowInternal,
73
74
  });
74
75
  }
75
76
 
@@ -118,7 +119,7 @@ function create(config) {
118
119
  var body = _serializeBatch(batch, cfg.bodyShape);
119
120
  try {
120
121
  await retryHelper.withRetry(function () {
121
- return _post(cfg.url, body, headers, cfg.timeoutMs, cfg.allowedProtocols);
122
+ return _post(cfg.url, body, headers, cfg.timeoutMs, cfg.allowedProtocols, cfg.allowInternal);
122
123
  }, cfg.retry);
123
124
  } catch {
124
125
  // Batch permanently rejected — surface via the dropped counter.
package/lib/mail.js CHANGED
@@ -468,6 +468,7 @@ function httpTransport(opts) {
468
468
  var baseHeaders = opts.headers || {};
469
469
  var timeoutMs = opts.timeoutMs || 15000;
470
470
  var allowedProtocols = opts.allowedProtocols || null;
471
+ var allowInternal = opts.allowInternal != null ? opts.allowInternal : null;
471
472
  var interpret = typeof opts.interpret === "function" ? opts.interpret : null;
472
473
  var serialize = opts.serialize;
473
474
  var codePrefix = "mail/" + name;
@@ -505,6 +506,7 @@ function httpTransport(opts) {
505
506
  errorClass: MailError, // http-client constructs (code, message, permanent, statusCode)
506
507
  };
507
508
  if (allowedProtocols) reqOpts.allowedProtocols = allowedProtocols;
509
+ if (allowInternal !== null) reqOpts.allowInternal = allowInternal;
508
510
 
509
511
  var res;
510
512
  try {
@@ -568,6 +570,7 @@ function resendTransport(opts) {
568
570
  method: "POST",
569
571
  timeoutMs: opts.timeoutMs || 15000,
570
572
  allowedProtocols: opts.allowedProtocols || null,
573
+ allowInternal: opts.allowInternal != null ? opts.allowInternal : null,
571
574
  headers: {
572
575
  "Authorization": "Bearer " + opts.apiKey,
573
576
  "Content-Type": "application/json",
@@ -512,7 +512,7 @@ function httpClientEncrypted(opts) {
512
512
  headers["Content-Type"] = "application/json";
513
513
 
514
514
  var passThrough = {};
515
- var passable = ["allowedProtocols", "idleTimeoutMs", "maxResponseBytes",
515
+ var passable = ["allowedProtocols", "allowInternal", "idleTimeoutMs", "maxResponseBytes",
516
516
  "agent", "errorClass"];
517
517
  for (var i = 0; i < passable.length; i++) {
518
518
  if (reqOpts[passable[i]] !== undefined) passThrough[passable[i]] = reqOpts[passable[i]];
package/lib/notify.js CHANGED
@@ -187,6 +187,7 @@ function httpJson(opts) {
187
187
  throw _err("BAD_OPT", "notify.transports.httpJson: url is required");
188
188
  }
189
189
  var allowedProtocols = opts.allowHttp ? safeUrl.ALLOW_HTTP_ALL : safeUrl.ALLOW_HTTP_TLS;
190
+ var allowInternal = opts.allowInternal != null ? opts.allowInternal : null;
190
191
  // Validate URL at create time so bad URLs surface at boot, not at first send.
191
192
  safeUrl.parse(opts.url, { allowedProtocols: allowedProtocols, errorClass: NotifyError });
192
193
 
@@ -238,6 +239,7 @@ function httpJson(opts) {
238
239
  headers: headers,
239
240
  body: body,
240
241
  allowedProtocols: allowedProtocols,
242
+ allowInternal: allowInternal,
241
243
  errorClass: NotifyError,
242
244
  });
243
245
  var status = (res && (res.statusCode || res.status)) || 0;
@@ -73,6 +73,7 @@ function _httpRequest(method, urlObj, headers, body, opts) {
73
73
  maxResponseBytes: opts && opts.maxResponseBytes,
74
74
  errorClass: ObjectStoreError,
75
75
  allowedProtocols: opts && opts.allowedProtocols,
76
+ ...((opts && opts.allowInternal !== undefined) ? { allowInternal: opts.allowInternal } : {}),
76
77
  });
77
78
  }
78
79
 
@@ -193,8 +194,10 @@ function create(config) {
193
194
  // HTTPS-only by default — real Azure is always HTTPS. Operators with
194
195
  // an Azurite emulator endpoint opt in via config.allowedProtocols.
195
196
  var allowedProtocols = config.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
197
+ var allowInternal = config.allowInternal != null ? config.allowInternal : null;
196
198
  safeUrl.parse(endpoint, { allowedProtocols: allowedProtocols, errorClass: ObjectStoreError });
197
199
  var reqOpts = { timeoutMs: timeoutMs, allowedProtocols: allowedProtocols };
200
+ if (allowInternal !== null) reqOpts.allowInternal = allowInternal;
198
201
 
199
202
  function _blobUrl(key, params) {
200
203
  var u = new URL(endpoint + "/" + config.container + "/" + key);
@@ -81,6 +81,7 @@ function _httpRequest(method, urlObj, headers, body, opts) {
81
81
  maxResponseBytes: opts && opts.maxResponseBytes,
82
82
  errorClass: ObjectStoreError,
83
83
  allowedProtocols: opts && opts.allowedProtocols,
84
+ ...((opts && opts.allowInternal !== undefined) ? { allowInternal: opts.allowInternal } : {}),
84
85
  });
85
86
  }
86
87
 
@@ -143,9 +144,11 @@ function create(config) {
143
144
  // HTTPS-only by default — google APIs are always HTTPS. Operators with
144
145
  // an emulator / private fake-GCS endpoint opt in via config.allowedProtocols.
145
146
  var allowedProtocols = config.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
147
+ var allowInternal = config.allowInternal != null ? config.allowInternal : null;
146
148
  safeUrl.parse(endpoint, { allowedProtocols: allowedProtocols, errorClass: ObjectStoreError });
147
149
  safeUrl.parse(tokenEndpoint, { allowedProtocols: allowedProtocols, errorClass: ObjectStoreError });
148
150
  var reqOpts = { timeoutMs: timeoutMs, allowedProtocols: allowedProtocols };
151
+ if (allowInternal !== null) reqOpts.allowInternal = allowInternal;
149
152
 
150
153
  // ---- Token cache ----
151
154
  var cachedToken = null; // { accessToken, expiresAt }
@@ -44,6 +44,7 @@ function _request(method, url, body, headers, opts) {
44
44
  idleTimeoutMs: opts && opts.timeoutMs,
45
45
  errorClass: ObjectStoreError,
46
46
  allowedProtocols: opts && opts.allowedProtocols,
47
+ ...((opts && opts.allowInternal !== undefined) ? { allowInternal: opts.allowInternal } : {}),
47
48
  });
48
49
  }
49
50
 
@@ -67,6 +68,7 @@ function create(config) {
67
68
  // at first put(). HTTPS-only by default; cleartext appliances opt in
68
69
  // via config.allowedProtocols (safeUrl.ALLOW_HTTP_ALL).
69
70
  var allowedProtocols = config.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
71
+ var allowInternal = config.allowInternal != null ? config.allowInternal : null;
70
72
  safeUrl.parse(baseUrl, {
71
73
  allowedProtocols: allowedProtocols,
72
74
  errorClass: ObjectStoreError,
@@ -74,6 +76,7 @@ function create(config) {
74
76
  var headers = _authHeaders(config);
75
77
  var timeoutMs = config.timeoutMs;
76
78
  var reqOpts = { timeoutMs: timeoutMs, allowedProtocols: allowedProtocols };
79
+ if (allowInternal !== null) reqOpts.allowInternal = allowInternal;
77
80
 
78
81
  function put(key, body, _opts) {
79
82
  var url = _keyToUrl(baseUrl, key);
@@ -204,6 +204,7 @@ function _request(method, url, headers, body, opts) {
204
204
  maxResponseBytes: opts && opts.maxResponseBytes,
205
205
  errorClass: ObjectStoreError,
206
206
  allowedProtocols: opts && opts.allowedProtocols,
207
+ ...((opts && opts.allowInternal !== undefined) ? { allowInternal: opts.allowInternal } : {}),
207
208
  });
208
209
  }
209
210
 
@@ -223,11 +224,13 @@ function create(config) {
223
224
  // an internal cleartext S3-compatible endpoint (test fixtures, local
224
225
  // dev MinIO) opt in via config.allowedProtocols.
225
226
  var allowedProtocols = config.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
227
+ var allowInternal = config.allowInternal != null ? config.allowInternal : null;
226
228
  safeUrl.parse(endpoint, {
227
229
  allowedProtocols: allowedProtocols,
228
230
  errorClass: ObjectStoreError,
229
231
  });
230
232
  var reqOpts = { timeoutMs: config.timeoutMs, allowedProtocols: allowedProtocols };
233
+ if (allowInternal !== null) reqOpts.allowInternal = allowInternal;
231
234
 
232
235
  function _keyToUrl(key) {
233
236
  if (key.indexOf("\0") !== -1) throw _err("INVALID_KEY", "null byte in key", true);
@@ -0,0 +1,324 @@
1
+ "use strict";
2
+ /**
3
+ * ssrf-guard — outbound URL gate against private / loopback / link-local /
4
+ * cloud-metadata / reserved IP ranges.
5
+ *
6
+ * Wired as default-on in b.httpClient.request. Operators with internal
7
+ * mesh calls opt out per call:
8
+ *
9
+ * b.httpClient.request({ url: "http://internal.svc", allowInternal: true });
10
+ * b.httpClient.request({ url: "http://10.0.5.1", allowInternal: ["10.0.0.0/8"] });
11
+ *
12
+ * Standalone use:
13
+ *
14
+ * var ssrf = b.ssrfGuard;
15
+ * await ssrf.checkUrl("https://example.com"); // throws SsrfError on hit
16
+ * ssrf.classify("169.254.169.254"); // → "cloud-metadata"
17
+ * ssrf.cidrContains("10.0.0.0/8", "10.1.2.3"); // → true
18
+ *
19
+ * What's blocked by default:
20
+ * - IPv4 private (RFC 1918): 10/8, 172.16/12, 192.168/16
21
+ * - IPv4 loopback: 127/8
22
+ * - IPv4 link-local: 169.254/16
23
+ * - IPv4 reserved/broadcast: 0/8, 100.64/10 (CGNAT), 224/4 (multicast),
24
+ * 240/4, 255.255.255.255
25
+ * - IPv4 documentation/test: 192.0.2/24, 198.51.100/24, 203.0.113/24, 198.18/15
26
+ * - IPv6 loopback: ::1
27
+ * - IPv6 ULA (private): fc00::/7
28
+ * - IPv6 link-local: fe80::/10
29
+ * - Cloud metadata IPs: 169.254.169.254 (AWS/GCP/Azure),
30
+ * 169.254.170.2 (AWS ECS task role),
31
+ * fd00:ec2::254
32
+ *
33
+ * Hostnames are resolved via dns.lookup before classification, so a
34
+ * malicious hostname pointing at a private IP fails the guard. The
35
+ * resolved IP is the one used for the actual connection — DNS rebinding
36
+ * between this check and the connect is out of scope (operators with
37
+ * that threat model pin to IP literals or use a sealed internal DNS).
38
+ */
39
+
40
+ var dns = require("node:dns").promises;
41
+ var net = require("node:net");
42
+
43
+ var safeUrl = require("./safe-url");
44
+ var validateOpts = require("./validate-opts");
45
+
46
+ var { FrameworkError } = require("./framework-error");
47
+
48
+ class SsrfError extends FrameworkError {
49
+ constructor(message, code, ctx) {
50
+ super(message, code);
51
+ this.name = "SsrfError";
52
+ this.permanent = true;
53
+ this.isSsrfError = true;
54
+ if (ctx) {
55
+ this.url = ctx.url || null;
56
+ this.ip = ctx.ip || null;
57
+ this.category = ctx.category || null;
58
+ }
59
+ }
60
+ }
61
+
62
+ // ---- IPv4 ranges (as numeric prefix tables for fast match) ----
63
+ // Each entry: [networkInt, prefixLen]
64
+ var IPV4_PRIVATE = [
65
+ [_ipv4ToInt("10.0.0.0"), 8],
66
+ [_ipv4ToInt("172.16.0.0"), 12],
67
+ [_ipv4ToInt("192.168.0.0"), 16],
68
+ ];
69
+ var IPV4_LOOPBACK = [
70
+ [_ipv4ToInt("127.0.0.0"), 8],
71
+ ];
72
+ var IPV4_LINK_LOCAL = [
73
+ [_ipv4ToInt("169.254.0.0"), 16],
74
+ ];
75
+ var IPV4_RESERVED = [
76
+ [_ipv4ToInt("0.0.0.0"), 8], // "this network"
77
+ [_ipv4ToInt("100.64.0.0"), 10], // CGNAT (RFC 6598)
78
+ [_ipv4ToInt("192.0.0.0"), 24], // IETF protocol assignments
79
+ [_ipv4ToInt("192.0.2.0"), 24], // TEST-NET-1
80
+ [_ipv4ToInt("198.18.0.0"), 15], // network benchmark
81
+ [_ipv4ToInt("198.51.100.0"), 24], // TEST-NET-2
82
+ [_ipv4ToInt("203.0.113.0"), 24], // TEST-NET-3
83
+ [_ipv4ToInt("224.0.0.0"), 4], // multicast
84
+ [_ipv4ToInt("240.0.0.0"), 4], // reserved + 255.255.255.255
85
+ ];
86
+
87
+ // ---- IPv6 ranges (as 16-byte prefix tables) ----
88
+ var IPV6_LOOPBACK_BYTES = _ipv6ToBytes("::1");
89
+ var IPV6_UNSPECIFIED_BYTES = _ipv6ToBytes("::");
90
+ var IPV6_PRIVATE_PREFIX = _ipv6ToBytes("fc00::");
91
+ var IPV6_LINK_LOCAL_PREFIX = _ipv6ToBytes("fe80::");
92
+ var IPV6_DOC_PREFIX = _ipv6ToBytes("2001:db8::"); // documentation
93
+ var IPV6_V4_MAPPED_PREFIX = _ipv6ToBytes("::ffff:0:0"); // IPv4-mapped (::ffff:0:0/96)
94
+
95
+ // ---- Cloud metadata addresses (string-equality, exact match) ----
96
+ var CLOUD_METADATA_IPS = [
97
+ "169.254.169.254", // AWS, GCP, Azure, OpenStack, DO
98
+ "169.254.170.2", // AWS ECS task role
99
+ "fd00:ec2::254", // AWS IMDS over IPv6
100
+ ];
101
+
102
+ // ---- Helpers ----
103
+
104
+ function _ipv4ToInt(ip) {
105
+ var parts = ip.split(".");
106
+ return ((parts[0] | 0) << 24 >>> 0) +
107
+ ((parts[1] | 0) << 16) +
108
+ ((parts[2] | 0) << 8) +
109
+ (parts[3] | 0);
110
+ }
111
+
112
+ function _ipv6ToBytes(ip) {
113
+ // Node's net.isIPv6 returns 6 for valid IPv6; we then expand
114
+ // shorthand via manual parsing. node:net doesn't export an
115
+ // ipv6-to-bytes helper, but the URL constructor + Buffer dance
116
+ // is reliable for canonicalizing.
117
+ var groups = _expandIpv6(ip);
118
+ var out = Buffer.alloc(16);
119
+ for (var i = 0; i < 8; i++) {
120
+ var v = parseInt(groups[i], 16) || 0;
121
+ out[i * 2] = (v >> 8) & 0xff;
122
+ out[i * 2 + 1] = v & 0xff;
123
+ }
124
+ return out;
125
+ }
126
+
127
+ function _expandIpv6(ip) {
128
+ // Handle "::" zero-elision + IPv4-mapped suffix ("::ffff:1.2.3.4").
129
+ var lower = ip.toLowerCase();
130
+ // IPv4-mapped: convert trailing dotted-quad to two hex groups
131
+ var dot = lower.lastIndexOf(":");
132
+ if (dot !== -1 && lower.indexOf(".", dot) !== -1) {
133
+ var v4 = lower.slice(dot + 1);
134
+ var ipv4Int = _ipv4ToInt(v4);
135
+ var hi = (ipv4Int >>> 16) & 0xffff;
136
+ var lo = ipv4Int & 0xffff;
137
+ lower = lower.slice(0, dot + 1) + hi.toString(16) + ":" + lo.toString(16);
138
+ }
139
+ var doubleColon = lower.indexOf("::");
140
+ var leftStr, rightStr;
141
+ if (doubleColon === -1) {
142
+ leftStr = lower;
143
+ rightStr = "";
144
+ } else {
145
+ leftStr = lower.slice(0, doubleColon);
146
+ rightStr = lower.slice(doubleColon + 2);
147
+ }
148
+ var left = leftStr.length ? leftStr.split(":") : [];
149
+ var right = rightStr.length ? rightStr.split(":") : [];
150
+ var missing = 8 - left.length - right.length;
151
+ var fill = [];
152
+ for (var i = 0; i < missing; i++) fill.push("0");
153
+ return left.concat(fill).concat(right);
154
+ }
155
+
156
+ function _cidrIpv4Match(cidr, ip) {
157
+ var slash = cidr.indexOf("/");
158
+ if (slash === -1) return false;
159
+ var network = _ipv4ToInt(cidr.slice(0, slash));
160
+ var prefix = parseInt(cidr.slice(slash + 1), 10);
161
+ if (!Number.isFinite(prefix) || prefix < 0 || prefix > 32) return false;
162
+ var ipInt = _ipv4ToInt(ip);
163
+ if (prefix === 0) return true;
164
+ var mask = (0xffffffff << (32 - prefix)) >>> 0;
165
+ return (ipInt & mask) === (network & mask);
166
+ }
167
+
168
+ function _cidrIpv6Match(cidr, ip) {
169
+ var slash = cidr.indexOf("/");
170
+ if (slash === -1) return false;
171
+ var network = _ipv6ToBytes(cidr.slice(0, slash));
172
+ var prefix = parseInt(cidr.slice(slash + 1), 10);
173
+ if (!Number.isFinite(prefix) || prefix < 0 || prefix > 128) return false;
174
+ var bytes = _ipv6ToBytes(ip);
175
+ var fullBytes = Math.floor(prefix / 8);
176
+ var remainingBits = prefix % 8;
177
+ for (var i = 0; i < fullBytes; i++) {
178
+ if (bytes[i] !== network[i]) return false;
179
+ }
180
+ if (remainingBits > 0) {
181
+ var mask = (0xff << (8 - remainingBits)) & 0xff;
182
+ if ((bytes[fullBytes] & mask) !== (network[fullBytes] & mask)) return false;
183
+ }
184
+ return true;
185
+ }
186
+
187
+ function _ipv4PrefixMatch(prefixTable, ipInt) {
188
+ for (var i = 0; i < prefixTable.length; i++) {
189
+ var net4 = prefixTable[i][0];
190
+ var prefix = prefixTable[i][1];
191
+ var mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
192
+ if ((ipInt & mask) === (net4 & mask)) return true;
193
+ }
194
+ return false;
195
+ }
196
+
197
+ function _ipv6PrefixMatch(prefixBytes, prefixLen, ipBytes) {
198
+ var fullBytes = Math.floor(prefixLen / 8);
199
+ var remainingBits = prefixLen % 8;
200
+ for (var i = 0; i < fullBytes; i++) {
201
+ if (ipBytes[i] !== prefixBytes[i]) return false;
202
+ }
203
+ if (remainingBits > 0) {
204
+ var mask = (0xff << (8 - remainingBits)) & 0xff;
205
+ if ((ipBytes[fullBytes] & mask) !== (prefixBytes[fullBytes] & mask)) return false;
206
+ }
207
+ return true;
208
+ }
209
+
210
+ // ---- Public classification API ----
211
+
212
+ function classify(ip) {
213
+ if (typeof ip !== "string") return null;
214
+ var family = net.isIP(ip);
215
+ if (family === 0) return null;
216
+
217
+ if (CLOUD_METADATA_IPS.indexOf(ip) !== -1) return "cloud-metadata";
218
+
219
+ if (family === 4) {
220
+ var ipInt = _ipv4ToInt(ip);
221
+ if (_ipv4PrefixMatch(IPV4_LOOPBACK, ipInt)) return "loopback";
222
+ if (_ipv4PrefixMatch(IPV4_LINK_LOCAL, ipInt)) return "link-local";
223
+ if (_ipv4PrefixMatch(IPV4_PRIVATE, ipInt)) return "private";
224
+ if (_ipv4PrefixMatch(IPV4_RESERVED, ipInt)) return "reserved";
225
+ return null;
226
+ }
227
+
228
+ // IPv6
229
+ var bytes = _ipv6ToBytes(ip);
230
+ if (_bufEqual(bytes, IPV6_LOOPBACK_BYTES)) return "loopback";
231
+ if (_bufEqual(bytes, IPV6_UNSPECIFIED_BYTES)) return "reserved";
232
+ if (_ipv6PrefixMatch(IPV6_LINK_LOCAL_PREFIX, 10, bytes)) return "link-local";
233
+ if (_ipv6PrefixMatch(IPV6_PRIVATE_PREFIX, 7, bytes)) return "private";
234
+ if (_ipv6PrefixMatch(IPV6_DOC_PREFIX, 32, bytes)) return "reserved";
235
+ // IPv4-mapped addresses (::ffff:a.b.c.d/96): re-classify the v4 portion.
236
+ if (_ipv6PrefixMatch(IPV6_V4_MAPPED_PREFIX, 96, bytes)) {
237
+ var mappedV4 = bytes[12] + "." + bytes[13] + "." + bytes[14] + "." + bytes[15];
238
+ return classify(mappedV4);
239
+ }
240
+ return null;
241
+ }
242
+
243
+ function _bufEqual(a, b) {
244
+ if (a.length !== b.length) return false;
245
+ for (var i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
246
+ return true;
247
+ }
248
+
249
+ function cidrContains(cidr, ip) {
250
+ if (typeof cidr !== "string" || typeof ip !== "string") return false;
251
+ var slash = cidr.indexOf("/");
252
+ if (slash === -1) return false;
253
+ var network = cidr.slice(0, slash);
254
+ var nFamily = net.isIP(network);
255
+ var iFamily = net.isIP(ip);
256
+ if (nFamily === 0 || iFamily === 0 || nFamily !== iFamily) return false;
257
+ return nFamily === 4 ? _cidrIpv4Match(cidr, ip) : _cidrIpv6Match(cidr, ip);
258
+ }
259
+
260
+ // ---- URL check (DNS-resolving) ----
261
+
262
+ async function checkUrl(url, opts) {
263
+ opts = opts || {};
264
+ validateOpts(opts, ["allowInternal", "errorClass", "dnsLookup"], "ssrfGuard.checkUrl");
265
+
266
+ var ErrorClass = opts.errorClass || SsrfError;
267
+ var allowInternal = opts.allowInternal === true ? true :
268
+ Array.isArray(opts.allowInternal) ? opts.allowInternal :
269
+ false;
270
+
271
+ var parsed = url instanceof URL ? url : safeUrl.parse(String(url), {
272
+ allowedProtocols: safeUrl.ALLOW_HTTP_ALL,
273
+ errorClass: ErrorClass,
274
+ });
275
+ if (!parsed.hostname) {
276
+ throw new ErrorClass("URL '" + parsed.toString() + "' has no hostname",
277
+ "ssrf-guard/no-hostname", { url: parsed.toString() });
278
+ }
279
+
280
+ // Strip IPv6 brackets — net.isIP doesn't accept "[::1]" form, only "::1"
281
+ var hostForCheck = parsed.hostname.replace(/^\[|\]$/g, "");
282
+
283
+ var ips;
284
+ if (net.isIP(hostForCheck)) {
285
+ ips = [{ address: hostForCheck, family: net.isIP(hostForCheck) }];
286
+ } else {
287
+ var lookup = opts.dnsLookup || function (host) {
288
+ return dns.lookup(host, { all: true });
289
+ };
290
+ ips = await lookup(hostForCheck);
291
+ if (!Array.isArray(ips)) ips = [ips];
292
+ }
293
+
294
+ for (var i = 0; i < ips.length; i++) {
295
+ var addr = ips[i].address;
296
+ var category = classify(addr);
297
+ if (!category) continue;
298
+
299
+ if (allowInternal === true) continue;
300
+ if (Array.isArray(allowInternal) && allowInternal.some(function (cidr) {
301
+ return cidrContains(cidr, addr);
302
+ })) continue;
303
+
304
+ throw new ErrorClass(
305
+ "URL '" + parsed.toString() + "' resolves to " + addr +
306
+ " in the " + category + " range — pass allowInternal:true to override",
307
+ "ssrf-guard/blocked-" + category,
308
+ { url: parsed.toString(), ip: addr, category: category }
309
+ );
310
+ }
311
+ return { url: parsed, ips: ips };
312
+ }
313
+
314
+ module.exports = {
315
+ classify: classify,
316
+ cidrContains: cidrContains,
317
+ checkUrl: checkUrl,
318
+ isPrivate: function (ip) { return classify(ip) === "private"; },
319
+ isLoopback: function (ip) { return classify(ip) === "loopback"; },
320
+ isLinkLocal: function (ip) { return classify(ip) === "link-local"; },
321
+ isCloudMetadata: function (ip) { return classify(ip) === "cloud-metadata"; },
322
+ isReserved: function (ip) { return classify(ip) === "reserved"; },
323
+ SsrfError: SsrfError,
324
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",