← ./articles-ja

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が出る状態にします。

参考