Next.jsのlocalhost限定APIでX-Forwarded-Forを信用しない
localhost限定のAPIは便利機能であって、雑なセキュリティ境界ではありません。
Next.jsアプリで、ローカル実行中の管理者だけにセットアップ用tokenやQRコードを返したいことがあります。たとえば /api/auth-info でログインtokenを返す設計です。
これは、本当にlocalhostだけに制限されている場合に限って成立します。X-Forwarded-For を信じると危険です。
危険なendpoint: tokenを返すAPI
問題になる形はこれです。
GET /api/auth-info
返す内容がこうなら、remote callerに見えてはいけません。
{
"token": "secret-login-token",
"networkUrl": "http://192.168.1.20:3000"
}
remote callerがこのJSONを取得できるなら、認証は迂回されています。パスワードを推測する必要すらありません。
X-Forwarded-Forは証明ではない
X-Forwarded-For はproxyが転送元を伝えるためのheaderです。localhostから来た証明ではありません。
攻撃者はこう送れます。
curl -H "X-Forwarded-For: 127.0.0.1" https://example.com/api/auth-info
このheaderを直接信用すると、remote callerがlocalhostを名乗れます。
MDNも、X-Forwarded-For の不適切な利用はセキュリティリスクになると説明しています。trusted proxy chainを正しく定義していないなら、認証境界に使わないほうが安全です。
Hostでlocalhostを確認する
ローカル専用の便利endpointなら、まず Host を見ます。
function isLocalhostHost(host: string | null): boolean {
const hostname = (host ?? "").split(":")[0];
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
}
App Routerではこう使えます。
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",
token: "display-only-on-operator-machine",
});
}
重要なのは、X-Forwarded-For が理由でsecretを返さないことです。
remote userにはmanual flowを使う
分け方はこうです。
localhost visitor -> setup tokenやQRコードを見られる
remote visitor -> tokenを手入力する
remote requestには安全なmodeだけ返します。
{
"mode": "manual"
}
frontendで隠す前提にしてはいけません。JSONにtokenが入った時点で、remote callerは取得済みです。
spoof headerで検証する
攻撃者目線で確認します。
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
期待値は 403 Forbidden、またはtokenを含まないmanual responseです。
localhostも確認します。
curl -i http://localhost:3000/api/auth-info
operator machineだけでlocal convenience dataが出る状態にします。