Do Not Trust X-Forwarded-For for Localhost-Only Next.js Endpoints
A localhost-only endpoint is a convenience feature, not a security shortcut.
In a Next.js app, it is common to expose extra information only to the operator running the app on the same machine. For example, a local terminal app might show a setup token, a QR code, or a network URL from /api/auth-info.
That can be acceptable if the endpoint is truly restricted to localhost. It becomes dangerous if the restriction trusts X-Forwarded-For.
The risky endpoint pattern: returning local tokens
The risky shape is:
GET /api/auth-info
Returning something like:
{
"token": "secret-login-token",
"networkUrl": "http://192.168.1.20:3000"
}
If that endpoint is reachable by a remote caller, authentication is bypassed. The caller does not need to guess a password. The server hands them the token.
So the question is not "is this endpoint informational?" The question is "does this endpoint reveal anything that grants access?"
Why X-Forwarded-For is not an auth boundary
X-Forwarded-For is a forwarding hint used by proxies. It is not proof that a request came from localhost.
An attacker can send a header such as:
curl -H "X-Forwarded-For: 127.0.0.1" https://example.com/api/auth-info
If your access check trusts that header directly, the attacker can impersonate localhost.
MDN explicitly warns that improper use of X-Forwarded-For can create security risk. Use it for logging or rate-limit signals only when you understand your trusted proxy chain. Do not use raw client-supplied XFF as proof of local origin.
Prefer Host checks for local convenience endpoints
For a local-only convenience endpoint, a practical first check is the request host:
function isLocalhostHost(host: string | null): boolean {
const hostname = (host ?? "").split(":")[0];
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
}
In an App Router route:
import { headers } from "next/headers";
export async function GET() {
const host = (await headers()).get("host");
if (!isLocalhostHost(host)) {
return Response.json({ mode: "manual" }, { status: 403 });
}
return Response.json({
mode: "local",
networkUrl: "http://192.168.1.20:3000",
token: "display-only-on-operator-machine",
});
}
The exact implementation depends on your deployment. The important rule is that X-Forwarded-For should not be the reason this endpoint returns secrets.
Keep remote users on a manual auth flow
A safer split is:
localhost visitor -> can see setup token or QR code
remote visitor -> must enter token manually
For a remote request, return a mode that lets the UI show a manual form:
{
"mode": "manual"
}
Do not return the token and expect the frontend to hide it. If the JSON contains the token, the remote caller already has it.
Verify with spoofed headers
Test the endpoint as an attacker would:
curl -i https://example.com/api/auth-info
curl -i -H "X-Forwarded-For: 127.0.0.1" https://example.com/api/auth-info
curl -i -H "X-Forwarded-For: ::1" https://example.com/api/auth-info
Expected result:
HTTP/1.1 403 Forbidden
or a safe manual-mode response with no token.
Also test localhost:
curl -i http://localhost:3000/api/auth-info
Expected result: local convenience data only on the operator machine.
Limits: proxies, tunnels, and production hosting
Host checks are not a universal authentication system.
If you run behind a reverse proxy, tunnel, CDN, or custom domain, define the trust boundary explicitly. For production multi-user apps, use real authentication and authorization. A localhost exception should be a small operator convenience, not the main security model.
Also avoid putting tokens in URLs when possible. URLs appear in browser history, logs, proxy traces, and screenshots.