mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 20:20:49 +00:00
Without an explicit host, `server.listen(0)` binds to every interface, turning safe-chain's unauthenticated forward proxy into an open relay while `aikido-*` commands are running. Anyone reachable on the network can use it to hit the victim's localhost, intranet, or cloud metadata endpoints. The advertised HTTPS_PROXY URL already used `localhost` (loopback), but the listener itself was wide open. Bind to 127.0.0.1 explicitly and update the advertised URL to match. Add a regression test that verifies the listener refuses connections on non-loopback interfaces.
67 lines
2 KiB
JavaScript
67 lines
2 KiB
JavaScript
import { before, after, describe, it } from "node:test";
|
|
import assert from "node:assert";
|
|
import net from "node:net";
|
|
import os from "node:os";
|
|
import {
|
|
createSafeChainProxy,
|
|
mergeSafeChainProxyEnvironmentVariables,
|
|
} from "./registryProxy.js";
|
|
|
|
describe("registryProxy loopback binding", () => {
|
|
let proxy, proxyPort;
|
|
|
|
before(async () => {
|
|
proxy = createSafeChainProxy();
|
|
await proxy.startServer();
|
|
const envVars = mergeSafeChainProxyEnvironmentVariables([]);
|
|
proxyPort = parseInt(new URL(envVars.HTTPS_PROXY).port, 10);
|
|
});
|
|
|
|
after(async () => {
|
|
await proxy.stopServer();
|
|
});
|
|
|
|
it("advertises a loopback HTTPS_PROXY URL", () => {
|
|
const envVars = mergeSafeChainProxyEnvironmentVariables([]);
|
|
const hostname = new URL(envVars.HTTPS_PROXY).hostname;
|
|
assert.ok(
|
|
hostname === "127.0.0.1" || hostname === "::1" || hostname === "localhost",
|
|
`expected loopback hostname, got ${hostname}`
|
|
);
|
|
});
|
|
|
|
it("refuses connections on non-loopback interfaces", async () => {
|
|
const externalAddrs = Object.values(os.networkInterfaces())
|
|
.flat()
|
|
.filter((iface) => iface && iface.family === "IPv4" && !iface.internal)
|
|
.map((iface) => iface.address);
|
|
|
|
if (externalAddrs.length === 0) {
|
|
// No non-loopback interface available (e.g. locked-down CI) - skip.
|
|
return;
|
|
}
|
|
|
|
for (const addr of externalAddrs) {
|
|
await new Promise((resolve, reject) => {
|
|
const sock = net.createConnection({ host: addr, port: proxyPort });
|
|
const timer = setTimeout(() => {
|
|
sock.destroy();
|
|
resolve(); // Filtered / dropped is also fine - we just don't want success.
|
|
}, 500);
|
|
sock.once("connect", () => {
|
|
clearTimeout(timer);
|
|
sock.destroy();
|
|
reject(
|
|
new Error(
|
|
`proxy accepted a connection on non-loopback ${addr}:${proxyPort}`
|
|
)
|
|
);
|
|
});
|
|
sock.once("error", () => {
|
|
clearTimeout(timer);
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
});
|
|
});
|