
AIエージェントに指示を出して数分でWebアプリケーションや管理ダッシュボードを作り上げるバイブコーディングは、個人開発のスピードを劇的に加速させます。しかし、AIが自動生成したコードをそのまま公開・デプロイすると、外部から誰でもバックエンドAPIを呼び出せたり、ブラウザ側にAPIキーが露出したりする危険な状態に陥りがちです。
前回の理論編記事では、バイブコーディングにおけるセキュリティリスクとAIエージェント向けの標準規約づくりについて解説しました。今回はその実践編として、Next.jsとFastAPIを組み合わせたマルチクラウド構成を題材に、安全な多層防御アーキテクチャへと改修する具体的な手順を解説します。
個人開発のWebアプリやダッシュボードを安全に運用したい方や、AIが生成したコードの脆弱性をどのように防ぐべきか悩んでいる方は、ぜひ参考にしてください。
この記事でわかること
この記事を読むことで、以下の実装手順とセキュリティ設計が理解できます。
- Next.jsのRoute Handlerを活用してバックエンドAPIキーをブラウザから隠すサーバー側プロキシの構築方法
- Route Handler自体が誰でも叩けるオープンプロキシになるのを防ぐセッション認証と管理者権限の検証手順
- FastAPI側で許可オリジンを限定するCORS設定とタイミング攻撃を防ぐBearer認証の実装
- Cloud RunのIAM認証を利用してインターネットからの直接呼び出しを遮断する設計方針
- 内部ネットワークや不正な対象へのスキャン踏み台化を防ぐホワイトリストガードレールの設置
- 例外エラー発生時にスタックトレースや内部情報を漏洩させない安全なエラーハンドリング
- Google Apps Scriptなどの周辺ツールでコード内にキーを直書きしないプロパティ管理術
- curlコマンドを用いた未認証アクセスや不正オリジンの遮断テスト手順
前提条件と検証環境
本記事で解説するアーキテクチャと検証環境は以下の通りです。なお、セキュリティおよびプライバシー保護の観点から、記事内に登場するドメイン、IPアドレス、メールアドレス、APIキーはすべて安全なダミー値に置き換えています。
| 項目 | 採用技術・サービス | 役割 |
|---|---|---|
| フロントエンド | Next.js 15 (App Router / Vercel) | UI表示および認証付きプロキシ |
| バックエンド | FastAPI / Python 3.11 (Cloud Run) | 業務ロジックおよび監査API |
| データベース | Supabase (PostgreSQL) | RLSによるアクセス制御付きデータ管理 |
| 外部連携 | Google Apps Script (GAS) | 定期データ更新バッチ |
AIがやりがちな丸見えアーキテクチャの危険性
AIエージェントに「フロントエンドからバックエンドAPIを呼び出してデータを表示して」と依頼すると、以下のような構成を生成することがあります。
クライアントサイドのReactコンポーネント(ブラウザ)から、FastAPIのURL(例: https://your-api.run.app)を直接fetchで呼び出し、環境変数には NEXT_PUBLIC_API_KEY を定義してリクエストヘッダーに付与するパターンです。
さらに、ブラウザからの通信を成立させるため、FastAPI側のCORS設定には allow_origins=["*"](全オリジン許可)が記述されることもあります。
この構成には、以下の問題があります。
- APIキーの漏洩:
NEXT_PUBLIC_プレフィックスが付いた環境変数は、ブラウザ側のJavaScriptバンドルに平文で公開されます。開発者ツールのソースタブやネットワークタブから第三者が誰でもコピーできます。 - バックエンドへの直接アクセス: FastAPIがインターネットに公開されている場合、URLが判明すれば外部から誰でも直接リクエストできます。
- 踏み台化のリスク: バックエンドAPIにネットワーク診断やURL取得などの機能が含まれている場合、不正利用によって内部ネットワークへのスキャンや外部サーバーへの攻撃踏み台に悪用される危険があります。
CORSをワイルドカードにしたこと自体がAPIキー漏洩の直接原因ではありません。問題の本質は、秘密情報をブラウザ側に配置していること、そしてバックエンドの認証・認可が不十分であることです。
これらの問題を解決するために、フロントエンドとバックエンドの間に境界線を設ける多層防御を構築します。
実践対策1:Next.js Route Handlerでサーバー側プロキシを構築する
最初の対策は、ブラウザからFastAPIへの直接通信をやめ、Next.jsのサーバー側を経由させるサーバー側プロキシ(Route Handler)の導入です。
ブラウザは同一ドメインの /api/... にリクエストを送り、Next.jsのサーバーランタイム(Node.js環境)がプライベートな環境変数からAPIキーを読み出してバックエンドを呼び出します。これにより、ブラウザ側にFastAPIの実際のURLやAPIキーを渡さずに済みます。
ただし、Route Handlerを設置しただけでは認証機能にはなりません。
第三者もブラウザを経由せずに /api/audit へ直接HTTPリクエストを送信できるため、Route Handler内でログインセッションと管理者権限を必ず検証する必要があります。これを行わないと、自作アプリが誰でも利用できるオープンプロキシになってしまいます。
Route Handlerの実装例
Next.jsのApp Router環境で app/api/audit/route.ts を作成します。
セッション認証の確認、確認済みメールアドレスによる管理者権限の照合、CSRF対策(Origin検証)、ストリーム単位でのリクエストサイズ制限、Zodによる入力値検証、およびレスポンス読み込みまでを含めたタイムアウト制御を組み込んだ堅牢な実装例です。
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { createClient } from '@/utils/supabase/server';
export const runtime = 'nodejs';
// Zodによる入力値スキーマの厳格な検証
const requestSchema = z.object({
target_ip: z.string().ip(),
}).strict();
// ストリーム単位でリクエストサイズを読み込みながら制限する関数(メモリ枯渇防止)
async function readBodyWithLimit(
request: NextRequest,
maxBytes: number
): Promise<string | null> {
const reader = request.body?.getReader();
if (!reader) {
return null;
}
const chunks: Uint8Array[] = [];
let totalBytes = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
totalBytes += value.byteLength;
if (totalBytes > maxBytes) {
await reader.cancel();
throw new Error('BODY_TOO_LARGE');
}
chunks.push(value);
}
} finally {
reader.releaseLock();
}
const body = new Uint8Array(totalBytes);
let offset = 0;
for (const chunk of chunks) {
body.set(chunk, offset);
offset += chunk.byteLength;
}
return new TextDecoder().decode(body);
}
export async function POST(request: NextRequest) {
try {
// 1. サーバー側セッション認証の確認
const supabase = await createClient();
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
return NextResponse.json({ error: 'Unauthorized: ログインが必要です' }, { status: 401 });
}
// 2. Cookie認証に対するCSRF対策(Originヘッダーの厳格な検証)
const allowedOrigins = [
process.env.APP_ORIGIN,
...(process.env.NODE_ENV === 'development' ? ['http://localhost:3000'] : []),
].filter((value): value is string => Boolean(value));
const origin = request.headers.get('origin');
if (!origin || !allowedOrigins.includes(origin)) {
return NextResponse.json(
{ error: 'Forbidden: 不正なオリジンです' },
{ status: 403 }
);
}
// 3. 管理者権限の確認(確認済みメールアドレスのホワイトリスト照合)
const rawAllowedEmails = process.env.ALLOWED_EMAILS;
if (!rawAllowedEmails) {
console.error('Server configuration error: ALLOWED_EMAILS is not configured');
return NextResponse.json({ error: 'Server configuration error' }, { status: 500 });
}
const allowedEmails = rawAllowedEmails.split(',').map(e => e.trim().toLowerCase());
if (
!user.email ||
!user.email_confirmed_at ||
!allowedEmails.includes(user.email.toLowerCase())
) {
return NextResponse.json({ error: 'Forbidden: 管理者権限がありません' }, { status: 403 });
}
// 4. Content-Lengthヘッダーの事前確認
const contentLength = request.headers.get('content-length');
if (contentLength !== null) {
const length = Number(contentLength);
if (!Number.isFinite(length) || length < 0 || length > 4096) {
return NextResponse.json({ error: 'Request too large' }, { status: 413 });
}
}
// 5. ストリーム読み込みによるリクエストサイズ制限(最大4KB)
let rawBody: string | null;
try {
rawBody = await readBodyWithLimit(request, 4096);
} catch (readError) {
if (readError instanceof Error && readError.message === 'BODY_TOO_LARGE') {
return NextResponse.json({ error: 'Request too large' }, { status: 413 });
}
throw readError;
}
if (!rawBody) {
return NextResponse.json({ error: 'Empty request body' }, { status: 400 });
}
// 6. 入力値のバリデーション
let parsedBody;
try {
parsedBody = requestSchema.safeParse(JSON.parse(rawBody));
} catch {
return NextResponse.json({ error: 'Invalid JSON format' }, { status: 400 });
}
if (!parsedBody.success) {
return NextResponse.json({ error: 'Invalid request parameters' }, { status: 400 });
}
// 7. サーバー環境変数からバックエンド接続先とAPIキーを取得
const apiUrl = process.env.OPS_API_URL;
const apiKey = process.env.OPS_API_KEY;
if (!apiUrl || !apiKey) {
console.error('Server configuration error: Missing backend credentials');
return NextResponse.json({ error: 'Internal configuration error' }, { status: 500 });
}
const backendUrl = new URL('/audit/run', apiUrl);
if (backendUrl.protocol !== 'https:') {
return NextResponse.json(
{ error: 'Invalid backend URL configuration' },
{ status: 500 }
);
}
// 8. レスポンス読み込みまで含めたタイムアウト付きフェッチ(15秒)
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 15000);
try {
const backendResponse = await fetch(
backendUrl.toString(),
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify(parsedBody.data),
signal: controller.signal,
cache: 'no-store',
redirect: 'error',
}
);
if (!backendResponse.ok) {
console.error(`Backend API error: status ${backendResponse.status}`);
return NextResponse.json({ error: 'Backend request failed' }, { status: 502 });
}
const data = await backendResponse.json();
return NextResponse.json(data, {
headers: {
'Cache-Control': 'no-store',
},
});
} catch (fetchError) {
if (fetchError instanceof Error && fetchError.name === 'AbortError') {
return NextResponse.json({ error: 'Backend request timeout' }, { status: 504 });
}
throw fetchError;
} finally {
clearTimeout(timeout);
}
} catch (error: any) {
console.error('Proxy internal error:', error?.name || 'Unknown error');
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
バックエンドURLの組み立てには new URL('/audit/run', apiUrl) を使用し、さらにプロトコルがHTTPSであることを検証しています。これにより、正規表現のエスケープ問題や末尾スラッシュの有無による誤作動を排除し、平文HTTPへの誤送信による通信路漏洩リスクも防ぎます。
また、認証済みユーザーによるAPIの連続実行や不正利用を防ぐため、必要に応じてレート制限や実行回数の上限も設けてください。特にネットワーク診断など、バックエンド側で負荷の高い処理を実行するAPIではサーバーリソース保護の観点から重要です。
フロントエンド側の呼び出し修正
Reactコンポーネント(画面側)からは、外部のCloud Run URLではなく、相対パス /api/audit を呼び出すように変更します。
// 修正前(危険): 外部URLへ直接リクエストし、ブラウザにAPIキーを露出
// const res = await fetch('https://your-api.run.app/audit/run', { headers: { 'Authorization': `Bearer ${process.env.NEXT_PUBLIC_KEY}` } });
// 修正後(安全): 同一オリジンのRoute Handlerを経由し、Cookieセッションを自動送信
const response = await fetch('/api/audit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
target_ip: '192.168.1.1',
}),
credentials: 'same-origin',
});
if (!response.ok) {
throw new Error('API request failed');
}
const data = await response.json();
この改修により、ブラウザ側にバックエンドの実際の接続先やAPIキーを渡すことなく、認証されたユーザーだけが安全にバックエンドを実行できるようになります。
実践対策2:FastAPI側でのCORS厳格化とAPI認証
フロントエンド側をプロキシ化しても、バックエンドであるFastAPI自体がインターネット上に公開されている場合、外部から直接APIを叩かれるリスクが残ります。
FastAPI側でも多層的な防御を施します。
1. 許可オリジンを限定するCORS設定
ワイルドカード ["*"] を完全に廃止し、許可するフロントエンドのドメインのみを環境変数から読み込んで指定します。
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
allowed_origins_env = os.environ.get("ALLOWED_ORIGINS", "")
allowed_origins = [
origin.strip()
for origin in allowed_origins_env.split(",")
if origin.strip()
]
# 本番環境では許可オリジンが未設定なら起動させない(フェイルクローズ)
if not allowed_origins:
raise RuntimeError("ALLOWED_ORIGINS must be configured")
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=False,
allow_methods=["GET", "POST", "OPTIONS"],
allow_headers=["Authorization", "Content-Type", "X-API-Key"],
)
環境変数の設定例は以下の通りです。今回はBearer認証を使用するため、Cookieをクロスオリジンで送信する必要はなく、allow_credentials=False としています。
ALLOWED_ORIGINS=https://your-ops-dashboard.vercel.app
重要: CORSは認証機能ではありません。
CORSはブラウザが実行するクロスオリジン通信を制御する仕組みです。curlやPythonスクリプト、サーバー間通信などによる直接アクセスを防ぐものではありません。そのため、必ずAPI認証と組み合わせて使用します。
なお、今回の構成ではブラウザからFastAPIへ直接アクセスせず、Next.js Route Handlerを経由します。FastAPIへの通信はNext.jsサーバー(サーバー間)から行われるため、ブラウザのCORS制限は直接影響しません。したがって、ブラウザからFastAPIへ直接アクセスする別の用途がなければ、FastAPI側のCORS設定は削除しても差し支えありません。設定を残す場合でも、CORSはあくまでブラウザ向けの制御であり、バックエンド保護の主役はAPI認証であることを念頭に置く必要があります。
2. Bearer TokenによるAPIキー認証(タイミング攻撃対策)
FastAPIの Depends と HTTPBearer を活用し、有効なAPIキーを持たないリクエストを遮断します。文字列比較には、タイミング攻撃を防ぐ hmac.compare_digest を使用します。
import os
import hmac
from fastapi import Depends, HTTPException, Security, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
EXPECTED_API_KEY = os.environ.get("OPS_API_KEY")
if not EXPECTED_API_KEY:
raise RuntimeError("OPS_API_KEY must be configured")
security = HTTPBearer(auto_error=False)
async def verify_api_key(
credentials: HTTPAuthorizationCredentials | None = Security(security)
):
if credentials is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required",
headers={"WWW-Authenticate": "Bearer"},
)
if credentials.scheme.lower() != "bearer":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication scheme",
)
if not hmac.compare_digest(credentials.credentials, EXPECTED_API_KEY):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication token",
headers={"WWW-Authenticate": "Bearer"},
)
return True
各エンドポイントの引数に authenticated: bool = Depends(verify_api_key) を追加することで、認証されていないリクエストを確実に遮断できます。
APIキーは十分な長さを持つランダムな文字列として生成し、英数字などのASCII文字で構成することを推奨します(ASCII以外の文字を含む文字列の比較による予期せぬ例外を防ぐため)。また、定期的なキーローテーションと漏洩時の失効手順も用意してください。
3. Cloud RunのIAM認証も検討する
Cloud Runを利用している場合は、アプリケーションコード内のBearer認証だけでなく、Google Cloud側のIAM認証を利用する方法もあります。
Cloud Runには未認証アクセスを許可する設定と、IAM認証を必須にする設定があります。より強固な構成を目指す場合は、以下の設計が可能です。
- Cloud Runの未認証呼び出し(allUsers)を禁止する
- Next.js側のサーバーからGoogle Cloudのサービスアカウント認証情報を用いてIDトークン(OIDC)を取得する
- Cloud Runの起動権限(roles/run.invoker)を専用のサービスアカウントに限定する
なお、Cloud RunのIAM認証を有効にした場合、未認証アクセスはCloud Run側で拒否されるため、FastAPIのBearer認証による401応答とは異なる結果(Cloud RunのIAMによる401または403応答)になります。
また、Cloud RunのIAM認証とFastAPI独自のBearer認証を併用する場合、両方でAuthorizationヘッダーを使用すると競合します。Google Cloudではこのケースに対応するため、X-Serverless-Authorization ヘッダーを用意しています。
headers: {
'Content-Type': 'application/json',
// FastAPI側の独自認証
'Authorization': `Bearer ${apiKey}`,
// Cloud Run側のIAM認証
'X-Serverless-Authorization': `Bearer ${idToken}`,
}
Cloud Runは X-Serverless-Authorization でIAM認証を行い、FastAPI側には通常の Authorization ヘッダーが渡されます。
なお、上記はヘッダー設定部分のみの例です。実際には、Next.js側でGoogle Cloudの認証情報を利用してIDトークンを取得する処理が別途必要です。IDトークンのAudience(aud)には、呼び出し先Cloud RunサービスのURLを指定します。サービスアカウントの権限設定やトークンの取得方法も含めて構成してください。
VercelからCloud Runを呼び出す場合、サービスアカウントキーを利用する方法があります。ただし、秘密鍵の管理には漏洩リスクが伴います。より高度な運用では、VercelのOIDC認証情報とGoogle CloudのWorkload Identity Federationを連携させ、長期的な秘密鍵を保持せずに認証する方法も検討できます。個人開発などでIAM認証の導入が難しい場合でも、前述のアプリケーション層でのAPIキー認証を省略してよいわけではありません。
4. スキャンや踏み台化を防ぐIPアドレスガードレール
ネットワーク診断や外部連携機能を持つAPIでは、ユーザーが任意のリクエストパラメータを送信できると、攻撃者が社内IPや外部サーバーを指定してスキャンの踏み台に悪用するリスクがあります。
許可された対象のみを受け付けるホワイトリスト方式のガードレールを実装します。
import ipaddress
from pydantic import BaseModel, field_validator
ALLOWED_ROUTER_IPS = {
"192.168.1.1",
"192.168.0.1",
}
class AuditRequest(BaseModel):
target_ip: str
@field_validator("target_ip")
@classmethod
def validate_target_ip(cls, value: str) -> str:
try:
ip = ipaddress.ip_address(value)
except ValueError:
raise ValueError("Invalid IP address format")
normalized_ip = str(ip)
if normalized_ip not in ALLOWED_ROUTER_IPS:
raise ValueError("Target IP is not allowed")
return normalized_ip
IPアドレスをオブジェクトとして正規化したうえで、許可リストと照合します。
定義したリクエストモデルは、実際のエンドポイントでAPIキー認証と組み合わせて適用します。
from fastapi import Depends
@app.post("/audit/run")
async def run_audit(
request: AuditRequest,
authenticated: bool = Depends(verify_api_key),
):
# request.target_ip はAuditRequestにより正規化およびホワイトリスト検証済み
return await perform_audit(request.target_ip)
これにより、認証されたリクエストであっても、許可されたIPアドレス以外へのスキャン処理は実行されず、安全に遮断されます。
なお、プライベートIPアドレスを許可リストに登録しただけでは、Cloud Runから対象機器へ接続できるようにはなりません。実際にアクセスするには、Serverless VPC AccessやVPNなどの適切なネットワーク接続経路が別途必要です。
また、外部URLを取得する機能を実装する場合は、SSRF(Server-Side Request Forgery)対策として、localhost、プライベートIP、クラウドのメタデータサービス(169.254.169.254)などへのアクセスを遮断する必要があります。
加えて、DNS解決後のIPアドレスやリダイレクト先についても検証し、公開IP以外への接続を許可しないようにしてください。URL文字列だけを検査する方法では、DNS Rebindingなどの攻撃を防ぎきれない場合があります。
実践対策3:内部詳細エラーや例外メッセージの返却を防ぐ
AIがコードを生成する際、例外処理に return {"error": str(e)} と書くことが多くあります。
しかし、str(e) にはデータベースのテーブル構造、カラム名、SQLエラー文、接続先ホスト名、ファイルパスなどの内部情報が含まれることがあります。
安全なエラーハンドリングの基本は以下の2点です。
- 詳細な例外内容やスタックトレースは、アクセス制御されたサーバーログにのみ記録する
- クライアントへ返すJSONレスポンスには、汎用的なエラーメッセージのみを返す
import logging
from fastapi import HTTPException, status
logger = logging.getLogger(__name__)
async def execute_processing():
try:
result = await perform_internal_operation()
return result
except Exception:
# 詳細なエラーはサーバーログに記録(機密情報の混入には注意)
logger.exception("Internal processing failed")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal server error"
)
ただし、ログに出力する情報にも注意が必要です。APIキー、Cookie、個人情報、データベース接続文字列などがログに紛れ込まないように配慮してください。
実践対策4:周辺スクリプトやドキュメントのシークレット管理
Webアプリケーション本体が安全でも、スプレッドシート連携(Google Apps Scriptなど)や過去の設計ドキュメントに秘密鍵が平文で残っていると、Gitコミットなどを通じて情報が漏洩します。
特にGoogle Apps Script(GAS)では、コード内にトークンを文字列として直書きしてしまうケースがあります。
GASのスクリプトプロパティを活用した管理
GASでは、コードにキーを書くのではなく、エディタ画面の「プロジェクトの設定」にある「スクリプト プロパティ」にキーを保存し、コードから動的に呼び出します。
// スクリプトプロパティから安全に取得
const API_KEY = PropertiesService
.getScriptProperties()
.getProperty("STOCK_API_KEY");
const API_URL = PropertiesService
.getScriptProperties()
.getProperty("FETCH_API_URL");
function fetchLatestData() {
if (!API_KEY || !API_URL) {
console.error("Missing script properties");
return;
}
const options = {
method: "get",
headers: {
"Authorization": "Bearer " + API_KEY
},
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(API_URL, options);
const statusCode = response.getResponseCode();
if (statusCode !== 200) {
console.error("API request failed:", statusCode);
return;
}
// 取得データの処理...
}
この方法により、APIキーをソースコードに直接記述せずに管理できるため、GitやGitHubへのコミットによる秘密情報の漏洩リスクを低減できます。ただし、スクリプトの編集権限を持つユーザーはプロパティ値を参照できるため、編集権限は必要最小限に制限してください。
実践対策5:デプロイ後の攻撃遮断テスト(curlによる検証手順)
改修が完了してクラウド環境へデプロイした後は、意図した通りに不正アクセスが遮断されるかを実際の通信でテストします。
以下の4つのテストケースを実施します。
1. FastAPIへの未認証アクセスの遮断テスト
Authorizationヘッダーを付けずにバックエンドAPIへ直接リクエストを送信します。
curl -i -X POST \
"https://your-api-xxxxx.asia-northeast1.run.app/audit/run" \
-H "Content-Type: application/json" \
-d '{"target_ip":"192.168.1.1"}'
期待される結果: Cloud RunのIAM認証を無効にしている場合は、FastAPI側から 401 Unauthorized が返ることを確認します。IAM認証を有効にしている場合は、Cloud Run側で未認証リクエストが拒否され、401 または 403 が返ることを確認します。いずれの場合も、バックエンドの処理が実行されないことが重要です。
2. 不正なオリジンからのCORSテスト
許可されていない第三者のオリジンヘッダーを付与してプリフライトリクエストを送信します。
curl -i -X OPTIONS \
"https://your-api-xxxxx.asia-northeast1.run.app/audit/run" \
-H "Origin: https://malicious-site.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: authorization,content-type"
期待される結果: レスポンスに Access-Control-Allow-Origin: https://malicious-site.example.com が含まれないこと。なお、このテストはFastAPI側でCORSを設定している場合に実施します(CORS設定を削除した構成では対象外です)。また、curl自体はCORS制限を受けないため、CORSテストだけでAPIへの不正アクセスが遮断されたとは判断できない点に留意してください。
3. Next.js Route Handlerへの未認証アクセスの遮断テスト
Next.js側のAPIエンドポイントに直接リクエストを送信します。
curl -i -X POST \
"https://your-ops-dashboard.vercel.app/api/audit" \
-H "Content-Type: application/json" \
-d '{"target_ip":"192.168.1.1"}'
期待される結果: ログインセッションが存在しないため、401 Unauthorized(または管理者権限なしの 403 Forbidden)が返り、バックエンドのプロキシ処理が遮断されること。
4. 正常な認証済み通信のテスト
ログイン済みの管理者としてCookieセッションを保持したファイルを用いてAPIを呼び出します。
正常系のテストでは、実際にSupabase Authでログインした際にブラウザへ発行されたセッションCookieを使用します。Cookie名や値はプロジェクト環境やライブラリのバージョンによって異なるため、ブラウザの開発者ツールで確認するか、ログインセッションを保持したCookieファイルをcurlの -b オプションで指定してください。なお、認証Cookieの値を記事や共有資料などに平文で掲載しないよう注意してください。
curl -i -X POST \
"https://your-ops-dashboard.vercel.app/api/audit" \
-H "Content-Type: application/json" \
-H "Origin: https://your-ops-dashboard.vercel.app" \
-b cookies.txt \
-d '{"target_ip":"192.168.1.1"}'
cookies.txtには、実際にログインしたセッションのCookieを保存します。共有資料やGitリポジトリに含めないようにしてください。
期待される結果: 200 OK とともに期待したJSONレスポンスが正常に返ること。
よくある質問
個人開発や小規模なツール開発において、よくある疑問をまとめました。
Q1. 個人しか使わない管理画面でもRoute Handlerを挟む必要はありますか?
はい、強く推奨されます。個人利用であってもWeb上にデプロイされている以上、URLが判明すれば世界中からリクエストを送ることが可能です。Route Handlerを挟んでNext.jsのサーバー側でログインセッションや管理者権限を判定することで、バックエンドAPI用キーをブラウザに公開せずに済みます。ただし、Route Handler自体も公開URLであるため、認証・認可を省略してはいけません。
Q2. SupabaseのAnonキーはフロントエンドに出しても問題ないですか?
SupabaseのPublishableキー(従来のAnonキー)は、フロントエンドに配置することを想定して設計された公開可能キーです。ただし、それには「対象テーブルでRLS(行レベルセキュリティ)が正しく有効化されていること」が絶対条件です。なお、RLSを有効にするだけでなく、テーブルやビューに対するGRANT権限(PostgreSQL標準の権限設定)も必要最小限に設定してください。一方、Secretキー(従来のService Roleキー)はRLSをバイパスできる強力な権限を持つため、絶対にフロントエンドに出してはいけません。なお、Supabaseでは従来のAnon/Service Roleキーから新しいPublishable/Secretキーへの移行が進んでいます。
Q3. AIエージェントに最初からセキュアなコードを書かせるコツはありますか?
プロジェクト直下の AGENTS.md や GEMINI.md などのルール定義ファイルに、セキュリティ上の制約を事前に明記しておくことです。「バックエンドとの通信はRoute Handlerを経由しセッション検証を行うこと」「CORSのワイルドカードを禁止すること」「APIキーはサーバー側環境変数で管理しhmac比較を行うこと」などを環境ルールとして固定化するのが最も確実です。
まとめ
バイブコーディングの最大の魅力は、アイデアを瞬時に形にできる圧倒的な開発スピードにあります。しかし、スピードを優先するあまりセキュリティを軽視すると、個人開発であってもAPIの不正利用や秘密情報の漏洩といった深刻なトラブルに巻き込まれかねません。
今回解説した実践的な防御策をまとめます。
- フロントエンドとバックエンドの間にNext.js Route Handlerを挟み、APIキーをブラウザから隠す
- Route Handler側でセッション認証と管理者権限の確認を必ず実施する
- FastAPIのCORSを許可ドメインに限定し、Bearer認証(hmac比較)で未認証アクセスを遮断する
- 必要に応じてCloud RunのIAM認証を組み合わせる
- パラメータにはIPアドレスなどのホワイトリストガードレールを設け、踏み台化を防止する
- 例外エラー時は内部情報をレスポンスに含めず、ログ出力と汎用メッセージに留める
- GASや周辺スクリプトのトークンはスクリプトプロパティや環境変数で管理する
- curlなどを使って未認証アクセスや不正な入力が実際に遮断されることを確認する
重要なのは、セキュリティ対策を単一の仕組みに依存させない「多層防御」です。AIエージェントの力を最大限に活かしながら、安全で堅牢なWebアプリケーションを構築していきましょう。
次に読むおすすめ記事

参考情報
- Next.js App Router Route Handlers 公式ドキュメント: https://nextjs.org/docs/app/api-reference/file-conventions/route
- Next.js Authentication 公式ガイド: https://nextjs.org/docs/app/guides/authentication
- FastAPI CORS Tutorial 公式ドキュメント: https://fastapi.tiangolo.com/tutorial/cors/
- FastAPI Security First Steps 公式ドキュメント: https://fastapi.tiangolo.com/tutorial/security/first-steps/
- Google Cloud Run 認証設定公式ガイド: https://docs.cloud.google.com/run/docs/securing/managing-access
- Google Apps Script Properties Service 公式ドキュメント: https://developers.google.com/apps-script/guides/properties
- Supabase API Keys 公式ドキュメント: https://supabase.com/docs/guides/getting-started/api-keys
- Supabase Row Level Security 公式ドキュメント: https://supabase.com/docs/guides/database/postgres/row-level-security
- OWASP API Security Top 10: https://owasp.org/API-Security/editions/2023/en/0x11-t10/

