mirror of
https://github.com/Nystik-gh/ignis.git
synced 2026-06-17 04:35:53 +00:00
implement private host allow list
This commit is contained in:
@@ -80,6 +80,7 @@ To build from source instead of pulling the image, clone the repo and run `docke
|
||||
| `PGID` | Group ID for file ownership | `1000` |
|
||||
| `WRITE_COALESCE_MS` | Debounce window (ms) for rapid writes. On slow filesystems (rclone, NFS, SMB), set an appropriate duration. | `0` |
|
||||
| `WS_ORIGINS` | Comma-separated allowlist of `Origin` headers accepted on the WebSocket endpoint. When unset, any origin is accepted. | unset |
|
||||
| `PROXY_ALLOW_PRIVATE_HOSTS` | Comma-separated IPs or IPv4 CIDRs the cross-origin proxy may reach despite the private-address block, for LAN services. Matched against the resolved IP. Reopens SSRF to the listed targets. | unset |
|
||||
|
||||
Demo mode adds its own set of env vars (per-session vaults, auto-cleanup, proxy allowlist, login blocking). See [`examples/demo/`](examples/demo/) if you want to run a public demo deployment.
|
||||
|
||||
|
||||
@@ -52,8 +52,79 @@ function isPrivateIp(ip) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function ipv4ToInt(ip) {
|
||||
return ip
|
||||
.split(".")
|
||||
.reduce((acc, oct) => ((acc << 8) + Number(oct)) >>> 0, 0);
|
||||
}
|
||||
|
||||
// Parse PROXY_ALLOW_PRIVATE_HOSTS into matchers.
|
||||
// Exact IPs (v4 and v6) and IPv4 CIDRs are supported; IPv6 CIDR and malformed entries are ignored.
|
||||
function buildAllowList(entries) {
|
||||
const exact = new Set();
|
||||
const cidrV4 = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const slash = entry.indexOf("/");
|
||||
|
||||
if (slash === -1) {
|
||||
if (net.isIP(entry)) {
|
||||
exact.add(entry);
|
||||
} else {
|
||||
console.warn(
|
||||
"[proxy] ignoring invalid PROXY_ALLOW_PRIVATE_HOSTS entry:",
|
||||
entry,
|
||||
);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const base = entry.slice(0, slash);
|
||||
const prefix = Number(entry.slice(slash + 1));
|
||||
|
||||
if (
|
||||
net.isIP(base) === 4 &&
|
||||
Number.isInteger(prefix) &&
|
||||
prefix >= 0 &&
|
||||
prefix <= 32
|
||||
) {
|
||||
const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
|
||||
cidrV4.push({ network: (ipv4ToInt(base) & mask) >>> 0, mask });
|
||||
} else {
|
||||
console.warn(
|
||||
"[proxy] ignoring unsupported PROXY_ALLOW_PRIVATE_HOSTS entry:",
|
||||
entry,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { exact, cidrV4 };
|
||||
}
|
||||
|
||||
function allowsAddress(allow, ip) {
|
||||
if (allow.exact.has(ip)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (net.isIP(ip) === 4) {
|
||||
const value = ipv4ToInt(ip);
|
||||
|
||||
for (const { network, mask } of allow.cidrV4) {
|
||||
if ((value & mask) >>> 0 === network) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const privateAllowList = buildAllowList(settings.get("proxyAllowPrivate"));
|
||||
|
||||
// A public address always passes; a private one passes only when listed it in PROXY_ALLOW_PRIVATE_HOSTS.
|
||||
function addressAllowed(ip) {
|
||||
return !isPrivateIp(ip);
|
||||
return !isPrivateIp(ip) || allowsAddress(privateAllowList, ip);
|
||||
}
|
||||
|
||||
function httpError(status, message) {
|
||||
@@ -354,3 +425,5 @@ router.post("/", async (req, res) => {
|
||||
module.exports = router;
|
||||
module.exports.isPrivateIp = isPrivateIp;
|
||||
module.exports.proxyRequest = proxyRequest;
|
||||
module.exports.buildAllowList = buildAllowList;
|
||||
module.exports.allowsAddress = allowsAddress;
|
||||
|
||||
@@ -2,7 +2,8 @@ import { describe, it, expect } from "vitest";
|
||||
import { createRequire } from "module";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { isPrivateIp, proxyRequest } = require("./proxy.js");
|
||||
const { isPrivateIp, proxyRequest, buildAllowList, allowsAddress } =
|
||||
require("./proxy.js");
|
||||
|
||||
describe("isPrivateIp", () => {
|
||||
it("flags private and link-local IPv4", () => {
|
||||
@@ -74,3 +75,24 @@ describe("proxyRequest guard", () => {
|
||||
).rejects.toMatchObject({ statusCode: 403 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("proxy private-host allow list", () => {
|
||||
it("allows exact IPs and IPv4 CIDRs, rejects everything else", () => {
|
||||
const allow = buildAllowList(["192.168.0.0/16", "10.1.2.3", "::1"]);
|
||||
|
||||
expect(allowsAddress(allow, "192.168.1.5")).toBe(true);
|
||||
expect(allowsAddress(allow, "192.169.0.1")).toBe(false);
|
||||
expect(allowsAddress(allow, "10.1.2.3")).toBe(true);
|
||||
expect(allowsAddress(allow, "10.1.2.4")).toBe(false);
|
||||
expect(allowsAddress(allow, "::1")).toBe(true);
|
||||
expect(allowsAddress(allow, "8.8.8.8")).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores IPv6 CIDR and malformed entries", () => {
|
||||
const allow = buildAllowList(["fd00::/8", "garbage", "192.168.0.0/33"]);
|
||||
|
||||
expect(allow.exact.size).toBe(0);
|
||||
expect(allow.cidrV4.length).toBe(0);
|
||||
expect(allowsAddress(allow, "fd00::1")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,8 @@ const DEFAULTS = {
|
||||
// Empty allows any public host.
|
||||
proxyAllowlist: [],
|
||||
wsOrigins: [],
|
||||
// Private IPs/CIDRs the proxy may reach despite the SSRF guard.
|
||||
proxyAllowPrivate: [],
|
||||
};
|
||||
|
||||
const PROXY_MODES = ["any", "allowlist", "disabled"];
|
||||
@@ -24,7 +26,7 @@ const PROXY_MODES = ["any", "allowlist", "disabled"];
|
||||
const KEYS = Object.keys(DEFAULTS);
|
||||
|
||||
// Env vars only; never persisted to the settings file.
|
||||
const ENV_ONLY_KEYS = ["wsOrigins"];
|
||||
const ENV_ONLY_KEYS = ["wsOrigins", "proxyAllowPrivate"];
|
||||
|
||||
// Hard ceiling for request bodies.
|
||||
const MAX_BODY_BACKSTOP = 500 * 1024 * 1024;
|
||||
@@ -51,6 +53,10 @@ function fromEnv() {
|
||||
env.wsOrigins = parseList(process.env.WS_ORIGINS);
|
||||
}
|
||||
|
||||
if (process.env.PROXY_ALLOW_PRIVATE_HOSTS) {
|
||||
env.proxyAllowPrivate = parseList(process.env.PROXY_ALLOW_PRIVATE_HOSTS);
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user