fix(security): require proof that x-9r-real-ip came from the socket (GHSA-pjm4-8fpg-f9p6)
x-9r-real-ip and the Host fallback were trusted from client-controlled
headers whenever custom-server.js was not in the request path (npm run
start, start:bun), letting a remote caller pose as local to skip API key
auth and reach LOCAL_ONLY_PATHS (/api/mcp/*, /api/tunnel/enable,
/api/auth/reset-password).
custom-server.js now generates a per-process secret at boot and stamps it
as x-9r-peer-token on every request it sanitizes. hasTrustedPeerHeaders()
(src/lib/auth/trustedPeer.js) gates trust in x-9r-real-ip on that secret;
otherwise the guard falls back to Host only in development, and fails
closed in production. Same gate on loginLimiter.getClientIp() so a spoofed
header cannot rotate the login lockout bucket.
Also: fix isLoopbackHostname for IPv6 (::1, ::ffff:127.0.0.1) which the
old split(":")[0] reduced to empty string; route npm run start /
start:bun through custom-server.js (postbuild copies it into
.next/standalone, build-cli.js fails without it) so documented deployments
keep passwordless local access.
This commit is contained in:
committed by
decolua
parent
b04c03c6b5
commit
92259214db
@@ -1,9 +1,18 @@
|
||||
const http = require("http");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const crypto = require("crypto");
|
||||
const { pathToFileURL } = require("url");
|
||||
|
||||
const origCreate = http.createServer.bind(http);
|
||||
|
||||
// Per-process secret proving x-9r-real-ip was stamped below rather than sent by the client.
|
||||
// A bare `next start` / `next dev` never loads this file, so it cannot produce a matching
|
||||
// header even though the env var is inherited by child processes. Named like x-9r-cli-token
|
||||
// so the request-detail header sanitizer redacts it too.
|
||||
const PEER_TOKEN = crypto.randomBytes(24).toString("hex");
|
||||
process.env.NINEROUTER_PEER_TOKEN = PEER_TOKEN;
|
||||
|
||||
let backgroundRefreshStarted = false;
|
||||
|
||||
function startBackgroundTokenRefreshFromCustomServer() {
|
||||
@@ -57,7 +66,9 @@ http.createServer = (...args) => {
|
||||
delete req.headers["x-9r-real-ip"];
|
||||
delete req.headers["x-forwarded-for"];
|
||||
delete req.headers["x-9r-via-proxy"];
|
||||
delete req.headers["x-9r-peer-token"];
|
||||
req.headers["x-9r-real-ip"] = ip;
|
||||
req.headers["x-9r-peer-token"] = PEER_TOKEN;
|
||||
if (viaProxy) req.headers["x-9r-via-proxy"] = "1";
|
||||
return handler(req, res);
|
||||
};
|
||||
@@ -114,4 +125,15 @@ http.createServer = (...args) => {
|
||||
return server;
|
||||
};
|
||||
|
||||
if (require.main === module) require("./server.js");
|
||||
if (require.main === module) {
|
||||
const standalone = path.join(__dirname, "server.js");
|
||||
if (fs.existsSync(standalone)) {
|
||||
require(standalone);
|
||||
} else {
|
||||
// Repo checkout has no standalone build next to us. `next start` builds its HTTP
|
||||
// server in-process, so the wrapper above still sanitizes every request.
|
||||
const nextBin = require.resolve("next/dist/bin/next");
|
||||
process.argv = [process.argv[0], nextBin, "start", ...process.argv.slice(2)];
|
||||
require(nextBin);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user