@blamejs/core 0.5.4 → 0.5.6

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
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.5.x
10
10
 
11
+ - **0.5.5** (2026-04-30) — strict default CSP + IPv6 special-range expansion
12
+ - **0.5.4** (2026-04-30) — close SSRF DNS-rebinding window with pinned outbound DNS
11
13
  - **0.5.3** (2026-04-30) — security cleanup: trustProxy primitive + Vary merge + HSTS gate
12
14
  - **0.5.2** (2026-04-30) — b.breakGlass: passkey factor + service-account bypass + admin tools
13
15
  - **0.5.1** (2026-04-30) — b.breakGlass: per-cell encryption + context binding + migrate
@@ -39,6 +39,7 @@
39
39
  */
40
40
  var audit = require("./audit");
41
41
  var C = require("./constants");
42
+ var cache = require("./cache");
42
43
  var clusterStorage = require("./cluster-storage");
43
44
  var { generateBytes, generateToken, kdf, sha3Hash, encryptPacked, decryptPacked } = require("./crypto");
44
45
  var cryptoField = require("./crypto-field");
@@ -76,6 +77,10 @@ var ALLOWED_REASON_STORAGE = ["cleartext", "hmac", "both"];
76
77
  // Populated on first access per-table; invalidated on policy.set/delete.
77
78
  var policyCache = new Map(); // table -> policy
78
79
  var initialized = false;
80
+ // Framework-wide trustProxy setting (set at init). When true, the
81
+ // break-glass primitive consults X-Forwarded-For to populate the
82
+ // grant row's `ip` field — same trust boundary as middleware.
83
+ var _trustProxy = false;
79
84
 
80
85
  // Factor lockout — wrap auth.lockout so a hostile actor brute-forcing
81
86
  // TOTP codes against break-glass gets shut out after a few failures.
@@ -85,7 +90,6 @@ var _factorLockout = null;
85
90
  var _factorLockoutCache = null;
86
91
  function _ensureFactorLockout() {
87
92
  if (_factorLockout) return _factorLockout;
88
- var cache = require("./cache");
89
93
  _factorLockoutCache = cache.create({
90
94
  namespace: "breakglass.factor",
91
95
  backend: "memory",
@@ -305,10 +309,12 @@ async function migrate(table, opts) {
305
309
 
306
310
  function init(opts) {
307
311
  opts = opts || {};
308
- validateOpts(opts, ["now"], "breakGlass.init");
312
+ validateOpts(opts, ["now", "trustProxy"], "breakGlass.init");
309
313
  initialized = true;
310
314
  policyCache.clear();
311
315
  _factorLockout = null;
316
+ _trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
317
+ ? opts.trustProxy : false;
312
318
  }
313
319
 
314
320
  function _resetForTest() {
@@ -320,6 +326,7 @@ function _resetForTest() {
320
326
  }
321
327
  _factorLockout = null;
322
328
  _factorLockoutCache = null;
329
+ _trustProxy = false;
323
330
  }
324
331
 
325
332
  function _requireInit() {
@@ -695,7 +702,11 @@ async function grant(opts) {
695
702
  var nowMs = Date.now();
696
703
  var grantId = "bg-" + generateToken(16);
697
704
  var sessionId = (opts.req && opts.req.session && opts.req.session.id) || null;
698
- var ipFromReq = (opts.req && opts.req.socket && opts.req.socket.remoteAddress) || null;
705
+ // Honor the framework-wide trustProxy setting from init() same
706
+ // boundary as middleware. Without trustProxy, X-Forwarded-For is
707
+ // ignored as attacker-forgeable, and the grant pins to the socket
708
+ // remoteAddress only.
709
+ var ipFromReq = requestHelpers.clientIp(opts.req, { trustProxy: _trustProxy });
699
710
 
700
711
  var grantRow = {
701
712
  _id: grantId,
@@ -46,10 +46,16 @@ var DEFAULT_PERMISSIONS = [
46
46
  "screen-wake-lock=()", "sync-xhr=()", "usb=()", "web-share=()", "xr-spatial-tracking=()",
47
47
  ];
48
48
 
49
+ // Strict CSP — no 'unsafe-inline' on script-src OR style-src. Operators
50
+ // with inline styles / scripts wire `b.middleware.cspNonce()` and use
51
+ // `{{ cspNonce }}` in their views (or req.cspNonce in handlers); the
52
+ // nonce hooks into the policy via the `csp` opt or a custom value.
53
+ // Operators in transitional environments who genuinely need inline
54
+ // styles set `csp` explicitly to the loose form.
49
55
  var DEFAULT_CSP =
50
56
  "default-src 'self'; " +
51
57
  "script-src 'self'; " +
52
- "style-src 'self' 'unsafe-inline'; " +
58
+ "style-src 'self'; " +
53
59
  "img-src 'self' data:; " +
54
60
  "font-src 'self'; " +
55
61
  "connect-src 'self'; " +
package/lib/ssrf-guard.js CHANGED
@@ -94,6 +94,23 @@ var IPV6_PRIVATE_PREFIX = _ipv6ToBytes("fc00::");
94
94
  var IPV6_LINK_LOCAL_PREFIX = _ipv6ToBytes("fe80::");
95
95
  var IPV6_DOC_PREFIX = _ipv6ToBytes("2001:db8::"); // documentation
96
96
  var IPV6_V4_MAPPED_PREFIX = _ipv6ToBytes("::ffff:0:0"); // IPv4-mapped (::ffff:0:0/96)
97
+ // Multicast ff00::/8 — RFC 4291 §2.7. Refused for outbound HTTP same as
98
+ // IPv4 multicast 224/4 — there's no legitimate fetch from a multicast
99
+ // destination on a production gateway.
100
+ var IPV6_MULTICAST_PREFIX = _ipv6ToBytes("ff00::");
101
+ // NAT64 well-known prefix 64:ff9b::/96 — RFC 6052. Translates to IPv4
102
+ // embedded in the lower 32 bits; the framework refuses since the
103
+ // underlying v4 address is what gets contacted and might be private.
104
+ // (Operators with NAT64 deployments flip allowInternal.)
105
+ var IPV6_NAT64_PREFIX = _ipv6ToBytes("64:ff9b::");
106
+ // 6to4 2002::/16 — RFC 3056. Carries an embedded IPv4 in bytes 2-5;
107
+ // hostile use is to tunnel through to a v4 destination the v4 guard
108
+ // would have refused. Refused.
109
+ var IPV6_6TO4_PREFIX = _ipv6ToBytes("2002::");
110
+ // Discard prefix 100::/64 — RFC 6666. Dropped by routers; fetching
111
+ // from it is operationally meaningless and a likely sign of mis-config
112
+ // or attempted exfil to a sinkhole.
113
+ var IPV6_DISCARD_PREFIX = _ipv6ToBytes("100::");
97
114
 
98
115
  // ---- Cloud metadata addresses (string-equality, exact match) ----
99
116
  var CLOUD_METADATA_IPS = [
@@ -235,6 +252,20 @@ function classify(ip) {
235
252
  if (_ipv6PrefixMatch(IPV6_LINK_LOCAL_PREFIX, 10, bytes)) return "link-local";
236
253
  if (_ipv6PrefixMatch(IPV6_PRIVATE_PREFIX, 7, bytes)) return "private";
237
254
  if (_ipv6PrefixMatch(IPV6_DOC_PREFIX, 32, bytes)) return "reserved";
255
+ if (_ipv6PrefixMatch(IPV6_MULTICAST_PREFIX, 8, bytes)) return "reserved";
256
+ if (_ipv6PrefixMatch(IPV6_DISCARD_PREFIX, 64, bytes)) return "reserved";
257
+ // 6to4 (2002::/16) embeds a v4 address in bytes 2–5; classify the
258
+ // embedded v4 so a 6to4-wrapped private/metadata address is refused
259
+ // for the same reason its v4 form would be.
260
+ if (_ipv6PrefixMatch(IPV6_6TO4_PREFIX, 16, bytes)) {
261
+ var v4From6to4 = bytes[2] + "." + bytes[3] + "." + bytes[4] + "." + bytes[5];
262
+ return classify(v4From6to4) || "reserved";
263
+ }
264
+ // NAT64 well-known prefix (64:ff9b::/96) embeds v4 in bytes 12–15.
265
+ if (_ipv6PrefixMatch(IPV6_NAT64_PREFIX, 96, bytes)) {
266
+ var v4FromNat64 = bytes[12] + "." + bytes[13] + "." + bytes[14] + "." + bytes[15];
267
+ return classify(v4FromNat64) || "reserved";
268
+ }
238
269
  // IPv4-mapped addresses (::ffff:a.b.c.d/96): re-classify the v4 portion.
239
270
  if (_ipv6PrefixMatch(IPV6_V4_MAPPED_PREFIX, 96, bytes)) {
240
271
  var mappedV4 = bytes[12] + "." + bytes[13] + "." + bytes[14] + "." + bytes[15];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",