@node9/proxy 2.16.0 → 2.16.2
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/dist/cli.js +274 -284
- package/dist/cli.mjs +274 -284
- package/dist/dashboard.mjs +4 -4
- package/dist/index.js +220 -215
- package/dist/index.mjs +220 -215
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -3416,6 +3416,211 @@ function analyzeShellCommand(command) {
|
|
|
3416
3416
|
}
|
|
3417
3417
|
return { actions, paths, allTokens };
|
|
3418
3418
|
}
|
|
3419
|
+
var SSRF_MAX_HOST = 253;
|
|
3420
|
+
function parseComponent(s) {
|
|
3421
|
+
if (!s) return null;
|
|
3422
|
+
if (/^0[xX][0-9a-fA-F]+$/.test(s)) return parseInt(s.slice(2), 16);
|
|
3423
|
+
if (s === "0") return 0;
|
|
3424
|
+
if (/^0[0-7]+$/.test(s)) return parseInt(s.slice(1), 8);
|
|
3425
|
+
if (/^[1-9][0-9]*$/.test(s)) return Number(s);
|
|
3426
|
+
return null;
|
|
3427
|
+
}
|
|
3428
|
+
function parseIpv4(input) {
|
|
3429
|
+
let s = input;
|
|
3430
|
+
if (s.endsWith(".")) s = s.slice(0, -1);
|
|
3431
|
+
if (!s) return null;
|
|
3432
|
+
const parts = s.split(".");
|
|
3433
|
+
if (parts.length > 4) return null;
|
|
3434
|
+
const vals = [];
|
|
3435
|
+
for (const p of parts) {
|
|
3436
|
+
const v = parseComponent(p);
|
|
3437
|
+
if (v === null || !Number.isFinite(v) || v < 0) return null;
|
|
3438
|
+
vals.push(v);
|
|
3439
|
+
}
|
|
3440
|
+
const n = vals.length;
|
|
3441
|
+
for (let i = 0; i < n - 1; i++) if (vals[i] > 255) return null;
|
|
3442
|
+
const last = vals[n - 1];
|
|
3443
|
+
const remainingBytes = 4 - (n - 1);
|
|
3444
|
+
const limit = Math.pow(256, remainingBytes);
|
|
3445
|
+
if (last >= limit) return null;
|
|
3446
|
+
let value = last;
|
|
3447
|
+
for (let i = 0; i < n - 1; i++) value += vals[i] * Math.pow(256, 3 - i);
|
|
3448
|
+
if (value > 4294967295) return null;
|
|
3449
|
+
return [value >>> 24 & 255, value >>> 16 & 255, value >>> 8 & 255, value & 255].join(".");
|
|
3450
|
+
}
|
|
3451
|
+
function expandIpv6(input) {
|
|
3452
|
+
const s = input.toLowerCase();
|
|
3453
|
+
if (!/^[0-9a-f:.]+$/.test(s)) return null;
|
|
3454
|
+
if ((s.match(/::/g) ?? []).length > 1) return null;
|
|
3455
|
+
let head = s;
|
|
3456
|
+
let tailV4 = null;
|
|
3457
|
+
const lastColon = s.lastIndexOf(":");
|
|
3458
|
+
const afterLast = s.slice(lastColon + 1);
|
|
3459
|
+
if (afterLast.includes(".")) {
|
|
3460
|
+
const dotted = parseIpv4(afterLast);
|
|
3461
|
+
if (!dotted) return null;
|
|
3462
|
+
const o = dotted.split(".").map(Number);
|
|
3463
|
+
tailV4 = [o[0] << 8 | o[1], o[2] << 8 | o[3]];
|
|
3464
|
+
head = s.slice(0, lastColon + 1) + "0";
|
|
3465
|
+
}
|
|
3466
|
+
const [lhs, rhs] = head.includes("::") ? head.split("::") : [head, null];
|
|
3467
|
+
const toGroups = (part) => {
|
|
3468
|
+
if (!part) return [];
|
|
3469
|
+
const out = [];
|
|
3470
|
+
for (const g of part.split(":")) {
|
|
3471
|
+
if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
|
|
3472
|
+
out.push(parseInt(g, 16));
|
|
3473
|
+
}
|
|
3474
|
+
return out;
|
|
3475
|
+
};
|
|
3476
|
+
const left = toGroups(lhs);
|
|
3477
|
+
if (left === null) return null;
|
|
3478
|
+
let right = [];
|
|
3479
|
+
if (rhs !== null) {
|
|
3480
|
+
const r = toGroups(rhs);
|
|
3481
|
+
if (r === null) return null;
|
|
3482
|
+
right = r;
|
|
3483
|
+
}
|
|
3484
|
+
if (tailV4) {
|
|
3485
|
+
if (rhs !== null) right = right.slice(0, -1).concat(tailV4);
|
|
3486
|
+
else left.splice(left.length - 1, 1, ...tailV4);
|
|
3487
|
+
}
|
|
3488
|
+
const groups = rhs === null ? left : left.concat(new Array(8 - left.length - right.length).fill(0), right);
|
|
3489
|
+
if (rhs === null && groups.length !== 8) return null;
|
|
3490
|
+
if (rhs !== null && left.length + right.length > 8) return null;
|
|
3491
|
+
if (groups.length !== 8) return null;
|
|
3492
|
+
return groups;
|
|
3493
|
+
}
|
|
3494
|
+
function compressIpv6(g) {
|
|
3495
|
+
let bestStart = -1;
|
|
3496
|
+
let bestLen = 0;
|
|
3497
|
+
let i = 0;
|
|
3498
|
+
while (i < 8) {
|
|
3499
|
+
if (g[i] !== 0) {
|
|
3500
|
+
i++;
|
|
3501
|
+
continue;
|
|
3502
|
+
}
|
|
3503
|
+
let j = i;
|
|
3504
|
+
while (j < 8 && g[j] === 0) j++;
|
|
3505
|
+
if (j - i > bestLen) {
|
|
3506
|
+
bestLen = j - i;
|
|
3507
|
+
bestStart = i;
|
|
3508
|
+
}
|
|
3509
|
+
i = j;
|
|
3510
|
+
}
|
|
3511
|
+
const hex = g.map((x) => x.toString(16));
|
|
3512
|
+
if (bestLen < 2) return hex.join(":");
|
|
3513
|
+
return hex.slice(0, bestStart).join(":") + "::" + hex.slice(bestStart + bestLen).join(":");
|
|
3514
|
+
}
|
|
3515
|
+
function normalizeIpLiteral(host) {
|
|
3516
|
+
try {
|
|
3517
|
+
if (typeof host !== "string") return null;
|
|
3518
|
+
let s = host.trim();
|
|
3519
|
+
if (!s || s.length > SSRF_MAX_HOST) return null;
|
|
3520
|
+
if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
|
|
3521
|
+
const zone = s.indexOf("%");
|
|
3522
|
+
if (zone >= 0) s = s.slice(0, zone);
|
|
3523
|
+
if (!s) return null;
|
|
3524
|
+
if (s.includes(":")) {
|
|
3525
|
+
const g = expandIpv6(s);
|
|
3526
|
+
if (!g) return null;
|
|
3527
|
+
const mapped = g.slice(0, 5).every((x) => x === 0) && g[5] === 65535;
|
|
3528
|
+
if (mapped) {
|
|
3529
|
+
return [g[6] >> 8 & 255, g[6] & 255, g[7] >> 8 & 255, g[7] & 255].join(".");
|
|
3530
|
+
}
|
|
3531
|
+
return compressIpv6(g);
|
|
3532
|
+
}
|
|
3533
|
+
return parseIpv4(s);
|
|
3534
|
+
} catch {
|
|
3535
|
+
return null;
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
var METADATA_ADDRESSES = /* @__PURE__ */ new Set([
|
|
3539
|
+
"169.254.169.254",
|
|
3540
|
+
// AWS / Azure / DigitalOcean / OpenStack IMDS
|
|
3541
|
+
"169.254.170.2",
|
|
3542
|
+
// AWS ECS task role
|
|
3543
|
+
"168.63.129.16",
|
|
3544
|
+
// Azure WireServer
|
|
3545
|
+
"fd00:ec2::254",
|
|
3546
|
+
// AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
|
|
3547
|
+
// Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
|
|
3548
|
+
// classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
|
|
3549
|
+
// live credential endpoint. It has to be named here, above the range check,
|
|
3550
|
+
// and it is the reason relaxing cgnat is safe.
|
|
3551
|
+
"100.100.100.200"
|
|
3552
|
+
]);
|
|
3553
|
+
var STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
|
|
3554
|
+
function isStrictGatedTier(tier) {
|
|
3555
|
+
return STRICT_TIERS.has(tier);
|
|
3556
|
+
}
|
|
3557
|
+
function ssrfReason(m, asWritten) {
|
|
3558
|
+
return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
|
|
3559
|
+
}
|
|
3560
|
+
var METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
|
|
3561
|
+
var v4Octets = (a) => {
|
|
3562
|
+
const p = a.split(".");
|
|
3563
|
+
return p.length === 4 ? p.map(Number) : null;
|
|
3564
|
+
};
|
|
3565
|
+
function classifySsrf(host) {
|
|
3566
|
+
try {
|
|
3567
|
+
if (typeof host !== "string" || !host) return null;
|
|
3568
|
+
const lower = host.trim().toLowerCase().replace(/\.$/, "");
|
|
3569
|
+
const ip = normalizeIpLiteral(host);
|
|
3570
|
+
if (ip === null) {
|
|
3571
|
+
return METADATA_HOSTNAMES.has(lower) ? { tier: "metadata", overridable: false, kind: "hostname" } : null;
|
|
3572
|
+
}
|
|
3573
|
+
const hit = (tier, overridable) => ({
|
|
3574
|
+
tier,
|
|
3575
|
+
overridable,
|
|
3576
|
+
kind: "address",
|
|
3577
|
+
normalized: ip
|
|
3578
|
+
});
|
|
3579
|
+
if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
|
|
3580
|
+
const o = v4Octets(ip);
|
|
3581
|
+
if (o) {
|
|
3582
|
+
if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
|
|
3583
|
+
if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
|
|
3584
|
+
if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
|
|
3585
|
+
if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
|
|
3586
|
+
if (o[0] === 127) return hit("private", true);
|
|
3587
|
+
if (o[0] === 10) return hit("private", true);
|
|
3588
|
+
if (o[0] === 192 && o[1] === 168) return hit("private", true);
|
|
3589
|
+
if (o[0] === 172 && o[1] >= 16 && o[1] <= 31) return hit("private", true);
|
|
3590
|
+
return null;
|
|
3591
|
+
}
|
|
3592
|
+
const g = expandIpv6(ip);
|
|
3593
|
+
if (!g) return null;
|
|
3594
|
+
if (g.every((x) => x === 0)) return hit("unspecified", true);
|
|
3595
|
+
if ((g[0] & 65472) === 65152) return hit("link-local", false);
|
|
3596
|
+
if ((g[0] & 65280) === 65280) return hit("multicast", false);
|
|
3597
|
+
if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
|
|
3598
|
+
return null;
|
|
3599
|
+
} catch {
|
|
3600
|
+
return null;
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
var TIER_REASON = {
|
|
3604
|
+
metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
|
|
3605
|
+
"link-local": "a link-local address",
|
|
3606
|
+
multicast: "a multicast address",
|
|
3607
|
+
unspecified: "the unspecified address, which reaches this host",
|
|
3608
|
+
cgnat: "a carrier-grade NAT address",
|
|
3609
|
+
private: "a loopback or private address"
|
|
3610
|
+
};
|
|
3611
|
+
function ssrfFloor(tokens, opts = {}) {
|
|
3612
|
+
const exempt = new Set(
|
|
3613
|
+
(opts.ssrfAllow ?? []).map((e) => normalizeIpLiteral(e) ?? e.trim().toLowerCase())
|
|
3614
|
+
);
|
|
3615
|
+
for (const { token, binary } of tokens) {
|
|
3616
|
+
const m = classifySsrf(token);
|
|
3617
|
+
if (!m) continue;
|
|
3618
|
+
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
3619
|
+
if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
|
|
3620
|
+
return { ...m, host: token, binary, reason: ssrfReason(m, token) };
|
|
3621
|
+
}
|
|
3622
|
+
return null;
|
|
3623
|
+
}
|
|
3419
3624
|
var DEFAULT_EGRESS_ALLOWLIST = [
|
|
3420
3625
|
// node9's own control plane (api, app, dev-api, staging and the apex).
|
|
3421
3626
|
// Without it, turning egress on asks the user to approve node9 itself.
|
|
@@ -3454,15 +3659,20 @@ function matchesAny(host, patterns) {
|
|
|
3454
3659
|
for (const p of patterns) if (hostMatches(host, p)) return true;
|
|
3455
3660
|
return false;
|
|
3456
3661
|
}
|
|
3662
|
+
var PRIVATE_HOST_SUFFIXES = [".local", ".internal", ".localhost"];
|
|
3663
|
+
function isUniqueLocalV6(host) {
|
|
3664
|
+
const ip = normalizeIpLiteral(host);
|
|
3665
|
+
if (!ip || !ip.includes(":")) return false;
|
|
3666
|
+
const g = expandIpv6(ip);
|
|
3667
|
+
return g !== null && (g[0] & 65024) === 64512;
|
|
3668
|
+
}
|
|
3457
3669
|
function isPrivateHost(host) {
|
|
3458
|
-
const h = host.toLowerCase();
|
|
3459
|
-
|
|
3460
|
-
if (
|
|
3461
|
-
if (
|
|
3462
|
-
if (
|
|
3463
|
-
|
|
3464
|
-
if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
|
|
3465
|
-
return false;
|
|
3670
|
+
const h = host.trim().toLowerCase();
|
|
3671
|
+
const m = classifySsrf(h);
|
|
3672
|
+
if (m) return m.kind === "address" && (m.tier === "private" || m.tier === "unspecified");
|
|
3673
|
+
if (h === "localhost") return true;
|
|
3674
|
+
if (PRIVATE_HOST_SUFFIXES.some((s) => h.endsWith(s))) return true;
|
|
3675
|
+
return isUniqueLocalV6(h);
|
|
3466
3676
|
}
|
|
3467
3677
|
function evaluateEgress(dests, policy) {
|
|
3468
3678
|
if (!policy.enabled) return null;
|
|
@@ -3805,211 +4015,6 @@ function extractAllSshHosts(tokens) {
|
|
|
3805
4015
|
}
|
|
3806
4016
|
return [...hosts].filter(Boolean);
|
|
3807
4017
|
}
|
|
3808
|
-
var SSRF_MAX_HOST = 253;
|
|
3809
|
-
function parseComponent(s) {
|
|
3810
|
-
if (!s) return null;
|
|
3811
|
-
if (/^0[xX][0-9a-fA-F]+$/.test(s)) return parseInt(s.slice(2), 16);
|
|
3812
|
-
if (s === "0") return 0;
|
|
3813
|
-
if (/^0[0-7]+$/.test(s)) return parseInt(s.slice(1), 8);
|
|
3814
|
-
if (/^[1-9][0-9]*$/.test(s)) return Number(s);
|
|
3815
|
-
return null;
|
|
3816
|
-
}
|
|
3817
|
-
function parseIpv4(input) {
|
|
3818
|
-
let s = input;
|
|
3819
|
-
if (s.endsWith(".")) s = s.slice(0, -1);
|
|
3820
|
-
if (!s) return null;
|
|
3821
|
-
const parts = s.split(".");
|
|
3822
|
-
if (parts.length > 4) return null;
|
|
3823
|
-
const vals = [];
|
|
3824
|
-
for (const p of parts) {
|
|
3825
|
-
const v = parseComponent(p);
|
|
3826
|
-
if (v === null || !Number.isFinite(v) || v < 0) return null;
|
|
3827
|
-
vals.push(v);
|
|
3828
|
-
}
|
|
3829
|
-
const n = vals.length;
|
|
3830
|
-
for (let i = 0; i < n - 1; i++) if (vals[i] > 255) return null;
|
|
3831
|
-
const last = vals[n - 1];
|
|
3832
|
-
const remainingBytes = 4 - (n - 1);
|
|
3833
|
-
const limit = Math.pow(256, remainingBytes);
|
|
3834
|
-
if (last >= limit) return null;
|
|
3835
|
-
let value = last;
|
|
3836
|
-
for (let i = 0; i < n - 1; i++) value += vals[i] * Math.pow(256, 3 - i);
|
|
3837
|
-
if (value > 4294967295) return null;
|
|
3838
|
-
return [value >>> 24 & 255, value >>> 16 & 255, value >>> 8 & 255, value & 255].join(".");
|
|
3839
|
-
}
|
|
3840
|
-
function expandIpv6(input) {
|
|
3841
|
-
const s = input.toLowerCase();
|
|
3842
|
-
if (!/^[0-9a-f:.]+$/.test(s)) return null;
|
|
3843
|
-
if ((s.match(/::/g) ?? []).length > 1) return null;
|
|
3844
|
-
let head = s;
|
|
3845
|
-
let tailV4 = null;
|
|
3846
|
-
const lastColon = s.lastIndexOf(":");
|
|
3847
|
-
const afterLast = s.slice(lastColon + 1);
|
|
3848
|
-
if (afterLast.includes(".")) {
|
|
3849
|
-
const dotted = parseIpv4(afterLast);
|
|
3850
|
-
if (!dotted) return null;
|
|
3851
|
-
const o = dotted.split(".").map(Number);
|
|
3852
|
-
tailV4 = [o[0] << 8 | o[1], o[2] << 8 | o[3]];
|
|
3853
|
-
head = s.slice(0, lastColon + 1) + "0";
|
|
3854
|
-
}
|
|
3855
|
-
const [lhs, rhs] = head.includes("::") ? head.split("::") : [head, null];
|
|
3856
|
-
const toGroups = (part) => {
|
|
3857
|
-
if (!part) return [];
|
|
3858
|
-
const out = [];
|
|
3859
|
-
for (const g of part.split(":")) {
|
|
3860
|
-
if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
|
|
3861
|
-
out.push(parseInt(g, 16));
|
|
3862
|
-
}
|
|
3863
|
-
return out;
|
|
3864
|
-
};
|
|
3865
|
-
const left = toGroups(lhs);
|
|
3866
|
-
if (left === null) return null;
|
|
3867
|
-
let right = [];
|
|
3868
|
-
if (rhs !== null) {
|
|
3869
|
-
const r = toGroups(rhs);
|
|
3870
|
-
if (r === null) return null;
|
|
3871
|
-
right = r;
|
|
3872
|
-
}
|
|
3873
|
-
if (tailV4) {
|
|
3874
|
-
if (rhs !== null) right = right.slice(0, -1).concat(tailV4);
|
|
3875
|
-
else left.splice(left.length - 1, 1, ...tailV4);
|
|
3876
|
-
}
|
|
3877
|
-
const groups = rhs === null ? left : left.concat(new Array(8 - left.length - right.length).fill(0), right);
|
|
3878
|
-
if (rhs === null && groups.length !== 8) return null;
|
|
3879
|
-
if (rhs !== null && left.length + right.length > 8) return null;
|
|
3880
|
-
if (groups.length !== 8) return null;
|
|
3881
|
-
return groups;
|
|
3882
|
-
}
|
|
3883
|
-
function compressIpv6(g) {
|
|
3884
|
-
let bestStart = -1;
|
|
3885
|
-
let bestLen = 0;
|
|
3886
|
-
let i = 0;
|
|
3887
|
-
while (i < 8) {
|
|
3888
|
-
if (g[i] !== 0) {
|
|
3889
|
-
i++;
|
|
3890
|
-
continue;
|
|
3891
|
-
}
|
|
3892
|
-
let j = i;
|
|
3893
|
-
while (j < 8 && g[j] === 0) j++;
|
|
3894
|
-
if (j - i > bestLen) {
|
|
3895
|
-
bestLen = j - i;
|
|
3896
|
-
bestStart = i;
|
|
3897
|
-
}
|
|
3898
|
-
i = j;
|
|
3899
|
-
}
|
|
3900
|
-
const hex = g.map((x) => x.toString(16));
|
|
3901
|
-
if (bestLen < 2) return hex.join(":");
|
|
3902
|
-
return hex.slice(0, bestStart).join(":") + "::" + hex.slice(bestStart + bestLen).join(":");
|
|
3903
|
-
}
|
|
3904
|
-
function normalizeIpLiteral(host) {
|
|
3905
|
-
try {
|
|
3906
|
-
if (typeof host !== "string") return null;
|
|
3907
|
-
let s = host.trim();
|
|
3908
|
-
if (!s || s.length > SSRF_MAX_HOST) return null;
|
|
3909
|
-
if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
|
|
3910
|
-
const zone = s.indexOf("%");
|
|
3911
|
-
if (zone >= 0) s = s.slice(0, zone);
|
|
3912
|
-
if (!s) return null;
|
|
3913
|
-
if (s.includes(":")) {
|
|
3914
|
-
const g = expandIpv6(s);
|
|
3915
|
-
if (!g) return null;
|
|
3916
|
-
const mapped = g.slice(0, 5).every((x) => x === 0) && g[5] === 65535;
|
|
3917
|
-
if (mapped) {
|
|
3918
|
-
return [g[6] >> 8 & 255, g[6] & 255, g[7] >> 8 & 255, g[7] & 255].join(".");
|
|
3919
|
-
}
|
|
3920
|
-
return compressIpv6(g);
|
|
3921
|
-
}
|
|
3922
|
-
return parseIpv4(s);
|
|
3923
|
-
} catch {
|
|
3924
|
-
return null;
|
|
3925
|
-
}
|
|
3926
|
-
}
|
|
3927
|
-
var METADATA_ADDRESSES = /* @__PURE__ */ new Set([
|
|
3928
|
-
"169.254.169.254",
|
|
3929
|
-
// AWS / Azure / DigitalOcean / OpenStack IMDS
|
|
3930
|
-
"169.254.170.2",
|
|
3931
|
-
// AWS ECS task role
|
|
3932
|
-
"168.63.129.16",
|
|
3933
|
-
// Azure WireServer
|
|
3934
|
-
"fd00:ec2::254",
|
|
3935
|
-
// AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
|
|
3936
|
-
// Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
|
|
3937
|
-
// classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
|
|
3938
|
-
// live credential endpoint. It has to be named here, above the range check,
|
|
3939
|
-
// and it is the reason relaxing cgnat is safe.
|
|
3940
|
-
"100.100.100.200"
|
|
3941
|
-
]);
|
|
3942
|
-
var STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
|
|
3943
|
-
function isStrictGatedTier(tier) {
|
|
3944
|
-
return STRICT_TIERS.has(tier);
|
|
3945
|
-
}
|
|
3946
|
-
function ssrfReason(m, asWritten) {
|
|
3947
|
-
return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
|
|
3948
|
-
}
|
|
3949
|
-
var METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
|
|
3950
|
-
var v4Octets = (a) => {
|
|
3951
|
-
const p = a.split(".");
|
|
3952
|
-
return p.length === 4 ? p.map(Number) : null;
|
|
3953
|
-
};
|
|
3954
|
-
function classifySsrf(host) {
|
|
3955
|
-
try {
|
|
3956
|
-
if (typeof host !== "string" || !host) return null;
|
|
3957
|
-
const lower = host.trim().toLowerCase().replace(/\.$/, "");
|
|
3958
|
-
const ip = normalizeIpLiteral(host);
|
|
3959
|
-
if (ip === null) {
|
|
3960
|
-
return METADATA_HOSTNAMES.has(lower) ? { tier: "metadata", overridable: false, kind: "hostname" } : null;
|
|
3961
|
-
}
|
|
3962
|
-
const hit = (tier, overridable) => ({
|
|
3963
|
-
tier,
|
|
3964
|
-
overridable,
|
|
3965
|
-
kind: "address",
|
|
3966
|
-
normalized: ip
|
|
3967
|
-
});
|
|
3968
|
-
if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
|
|
3969
|
-
const o = v4Octets(ip);
|
|
3970
|
-
if (o) {
|
|
3971
|
-
if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
|
|
3972
|
-
if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
|
|
3973
|
-
if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
|
|
3974
|
-
if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
|
|
3975
|
-
if (o[0] === 127) return hit("private", true);
|
|
3976
|
-
if (o[0] === 10) return hit("private", true);
|
|
3977
|
-
if (o[0] === 192 && o[1] === 168) return hit("private", true);
|
|
3978
|
-
if (o[0] === 172 && o[1] >= 16 && o[1] <= 31) return hit("private", true);
|
|
3979
|
-
return null;
|
|
3980
|
-
}
|
|
3981
|
-
const g = expandIpv6(ip);
|
|
3982
|
-
if (!g) return null;
|
|
3983
|
-
if (g.every((x) => x === 0)) return hit("unspecified", true);
|
|
3984
|
-
if ((g[0] & 65472) === 65152) return hit("link-local", false);
|
|
3985
|
-
if ((g[0] & 65280) === 65280) return hit("multicast", false);
|
|
3986
|
-
if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
|
|
3987
|
-
return null;
|
|
3988
|
-
} catch {
|
|
3989
|
-
return null;
|
|
3990
|
-
}
|
|
3991
|
-
}
|
|
3992
|
-
var TIER_REASON = {
|
|
3993
|
-
metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
|
|
3994
|
-
"link-local": "a link-local address",
|
|
3995
|
-
multicast: "a multicast address",
|
|
3996
|
-
unspecified: "the unspecified address, which reaches this host",
|
|
3997
|
-
cgnat: "a carrier-grade NAT address",
|
|
3998
|
-
private: "a loopback or private address"
|
|
3999
|
-
};
|
|
4000
|
-
function ssrfFloor(tokens, opts = {}) {
|
|
4001
|
-
const exempt = new Set(
|
|
4002
|
-
(opts.ssrfAllow ?? []).map((e) => normalizeIpLiteral(e) ?? e.trim().toLowerCase())
|
|
4003
|
-
);
|
|
4004
|
-
for (const { token, binary } of tokens) {
|
|
4005
|
-
const m = classifySsrf(token);
|
|
4006
|
-
if (!m) continue;
|
|
4007
|
-
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
|
|
4008
|
-
if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
|
|
4009
|
-
return { ...m, host: token, binary, reason: ssrfReason(m, token) };
|
|
4010
|
-
}
|
|
4011
|
-
return null;
|
|
4012
|
-
}
|
|
4013
4018
|
var DESTINATION_ARGS = /* @__PURE__ */ new Map([
|
|
4014
4019
|
["webfetch", ["url"]],
|
|
4015
4020
|
["fetch", ["url", "uri"]],
|
|
@@ -6199,14 +6204,14 @@ function getCredentials() {
|
|
|
6199
6204
|
const creds = JSON.parse(fs4.readFileSync(credPath, "utf-8"));
|
|
6200
6205
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
6201
6206
|
const profile = creds[profileName];
|
|
6202
|
-
if (profile?.apiKey) {
|
|
6207
|
+
if (typeof profile?.apiKey === "string" && profile.apiKey.length > 0) {
|
|
6203
6208
|
return {
|
|
6204
6209
|
apiKey: profile.apiKey,
|
|
6205
6210
|
apiUrl: safeApiUrl(profile.apiUrl || DEFAULT_API_URL, noteRejectedApiUrl),
|
|
6206
6211
|
localOnly: profile.localOnly === true || profileName !== "default"
|
|
6207
6212
|
};
|
|
6208
6213
|
}
|
|
6209
|
-
if (creds.apiKey) {
|
|
6214
|
+
if (typeof creds.apiKey === "string" && creds.apiKey.length > 0) {
|
|
6210
6215
|
return {
|
|
6211
6216
|
apiKey: creds.apiKey,
|
|
6212
6217
|
apiUrl: safeApiUrl(creds.apiUrl || DEFAULT_API_URL, noteRejectedApiUrl),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "2.16.
|
|
3
|
+
"version": "2.16.2",
|
|
4
4
|
"description": "IAM for your AI agents. Set what Claude Code, Codex, Gemini, Cursor and any MCP server are allowed to do, review risky actions before they run, and keep every action on the record.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|