FixVibe

// docs / security guides / hardening

AI コーディングツールで作ったアプリの守り方

Cursor、Claude Code、Lovable、Bolt、v0、Replit、または Windsurf を使用して構築したアプリの段階的な強化ガイド。 4 つのフェーズ: AI- で生成されたアプリが異なる失敗をする理由を理解し、即時にコードベース監査を実行し、デプロイ時に強化し、監視を継続します。意見があり、物語性があり、コピーできる実際の断片が含まれています。

AI-生成されたアプリの失敗が異なる理由

Vibe コード化されたアプリは安全です。障害モードは不注意ではなく構造的なものであるため、追加の監査パスが必要です。

  • Assistants inline hardcoded keys. You ask for a fix to an auth error and get a pasted Supabase example that assumes a service-role client. The key ends up at the top of a page component. Both the anon client and the service client coexist; both ship.
  • Generated servers default to permissive CORS. Generated Express / Fastify handlers often ship with cors({ origin: '*' }) because that's the fastest way to get a working preview. The middleware never gets a second pass.
  • Rules files get skipped. Firestore-backed projects generate the data model but rarely touch firestore.rules. Test-mode rules let anyone read and overwrite data until someone replaces them.
  • RLS never enters the migration. A generated Supabase schema and CRUD surface use the anon key, but ENABLE ROW LEVEL SECURITY never enters the migration. Anonymous users can read or write any row.
  • Handlers trust IDs. A generated GET /api/items/[id] reads the param and queries Postgres without verifying ownership. Active scans on a verified domain test for this (IDOR / BOLA).

即時監査: コードベースを grep してリスク パターンを調べる

何かを硬化する前に、すでに壊れているものを見つけてください。これらの grep にはそれぞれ 1 分もかかりません。

シークレットとプロバイダーキー

bash
grep -RIn 'NEXT_PUBLIC_SUPABASE_SERVICE' src/
grep -RIn 'sk_live_\|pk_live_\|STRIPE_SECRET' src/
grep -RIn 'sk-ant-\|^sk-' src/  # Anthropic / OpenAI
grep -RIn 'AIza\|AKIA' src/        # Google / AWS
grep -RIn 'eyJh[A-Za-z0-9_-]\{20,\}' src/  # JWT-shaped strings

ヒットした場合は、削除とキーのローテーションが必要です。 Provider ダッシュボード: Supabase → 設定 → API、Stripe → 開発者 → API キー、Anthropic / OpenAI コンソール。

データベースのアクセス制御

bash
# Supabase migrations
grep -RIn 'CREATE TABLE public\.' supabase/migrations/
grep -RIn 'ENABLE ROW LEVEL SECURITY\|FORCE ROW LEVEL SECURITY' supabase/migrations/

# Firebase / Firestore
cat firestore.rules  # confirm no `if true;` matches

すべての CREATE TABLE public.* には、一致する ENABLE ROW LEVEL SECURITY と少なくとも 1 つのポリシーが必要です。 Firestore ルールは読み取りの範囲を request.auth.uid に限定する必要があります。

認証とセッションの処理

bash
grep -RIn 'getSession()' src/   # should be getUser() server-side
grep -RIn 'localStorage\.\(set\|get\)Item.*token' src/
grep -RIn 'jwt.verify.*\(noVerify\|skipVerify\)' src/

サーバーでレンダリングされたルートは supabase.auth.getUser() を使用する必要があります。これはバックエンドで検証されます。 getSession() は未検証の Cookie を読み取ります。 localStorage のトークンは、ページ上で実行されるすべてのスクリプトからアクセスできます。

ヘッダーとミドルウェア

bash
# Confirm middleware location for src/ layouts
ls src/middleware.ts middleware.ts 2>&1

# Look for CSP and security headers
grep -RIn 'Content-Security-Policy\|Strict-Transport-Security' src/

src/ レイアウトでは、src/middleware.ts のみがピックアップされます。ミドルウェア ファイルがプロジェクト ルートにある場合、Next.js はそれを黙って無視し、CSP / 認証更新ロジックは実行されません。

導入時の強化

ソースがクリーンになったら、アプリが本番環境に到達する方法をロックダウンします。

ステップ 1: 別々の環境

Vercel: 3 つの環境 - Production (本番ドメイン)、プレビュー (PR / ステージング デプロイ)、開発 (ローカル)。それぞれが独自の環境変数セットを取得します。ライブ Stripe / Anthropic / Supabase キーはプレビューに到達しません。プレビュー キーは Production に到達しません。ブランチは自動的にプレビューにプッシュされます。 main にマージすると、Production にデプロイされます。

ステップ 2: ミドルウェアを介した厳密な CSP

リクエストごとに nonce を生成し、それを Content-Security-Policy に注入します。 Next.js は、x-nonce リクエスト ヘッダーを設定すると、そのノンスを独自のスクリプト タグに自動的に適用します。

ts
// src/middleware.ts
import { NextResponse, type NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const nonce = crypto.randomUUID().replace(/-/g, '');
  const csp = [
    `script-src 'nonce-${nonce}' 'strict-dynamic'`,
    `style-src 'self' 'unsafe-inline'`,
    `img-src 'self' data: https:`,
    `connect-src 'self' https://*.supabase.co`,
    `object-src 'none'`,
    `base-uri 'self'`,
    `frame-ancestors 'none'`,
  ].join('; ');

  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-nonce', nonce);

  const response = NextResponse.next({ request: { headers: requestHeaders } });
  response.headers.set('Content-Security-Policy', csp);
  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  return response;
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};

ステップ 3: すべてのパブリック テーブルに RLS を強制する

RLS isn't enabled by default on tables you create in SQL or migrations. Enable it on every exposed table and pair each one with explicit policies per role — that is what stops the anon and authenticated roles. FORCE only makes the table owner obey RLS too.

sql
-- supabase/migrations/XXXX_rls.sql
alter table public.profiles enable row level security;
alter table public.profiles force row level security;

create policy "profiles: read own"
  on public.profiles for select
  using (auth.uid() = id);

create policy "profiles: update own"
  on public.profiles for update
  using (auth.uid() = id)
  with check (auth.uid() = id);

ステップ 4: すべての API ルートでのサーバーのみの認証検証

状態を変更するすべての API ルートは、呼び出し元のサーバー側を supabase.auth.getUser() で検証します。ユーザー オブジェクトは user_id にとって信頼できる情報源になります。リクエスト本文がそれを設定することを決して信頼しないでください。

ts
// src/app/api/items/route.ts
import { NextResponse, type NextRequest } from 'next/server';
import { createClient } from '@/lib/supabase/server';

export async function POST(request: NextRequest) {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();
  if (!user) return NextResponse.json({ error: 'unauthorized' }, { status: 401 });

  const body = await request.json();
  const { data, error } = await supabase
    .from('items')
    .insert({ ...body, user_id: user.id })  // server-supplied, not from body
    .select()
    .single();

  if (error) return NextResponse.json({ error: error.message }, { status: 400 });
  return NextResponse.json(data);
}

ステップ 5: 分析をリバースプロキシする

Pro独自のドメインを介して分析を実行すると、広告ブロッカーが回避され、CSP connect-src 'self' の範囲が狭くなります。 PostHog、Plausible、Umami、カスタム イベント シンクでも同じパターンが機能します。

ts
// src/app/api/posthog/[...path]/route.ts
import { type NextRequest } from 'next/server';

const UPSTREAM = 'https://us.i.posthog.com';

export async function POST(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
  const { path } = await params;
  const url = `${UPSTREAM}/${path.join('/')}`;
  return fetch(url, {
    method: 'POST',
    headers: { 'content-type': req.headers.get('content-type') ?? 'application/json' },
    body: await req.text(),
  });
}

ステップ 6: 認証後のバウンスでのオープンリダイレクト ガード

サインイン/サインアップ フローは通常、next クエリ パラメーターを受け入れます。同一サイトのパスではないものはすべて拒否します。/ から始め、// は使用しないでください (プロトコル相対で、ユーザーをオフサイトに送ります)。

ts
function safeNext(raw: string | null): string {
  if (!raw) return '/dashboard';
  if (!raw.startsWith('/') || raw.startsWith('//')) return '/dashboard';
  return raw;
}

継続中: モニタリングと再スキャン

ドリフトはデプロイごとに発生します。セキュリティを、完了したチェックリストではなく、ループとして扱います。

実稼働ドメインを確認する

Dashboard → Domains → add your production domain → DNS TXT or HTTP-file verification. Active scans require Hobby or above; scheduled re-scans require Pro or Unlimited.

パッシブ再スキャンのスケジュールを設定する

Scheduled re-scans are available on Pro and Unlimited for verified domains. Free and Hobby scans are manual. Scheduled scans use your plan allowance. Configure completion email preferences and a scan.completed webhook if needed.

bash
# Or from CI, via the REST API:
curl -X POST https://fixvibe.app/api/v1/scans \
  -H "authorization: Bearer $FIXVIBE_TOKEN" \
  -H "content-type: application/json" \
  -d '{"target":"https://your-app.com"}'

API-アクティブ スキャンを有効にする (オプション)

自動アクティブ プローブ (SQLi / XSS / IDOR ウォーキング / など) が必要な場合は、[ダッシュボード] → [ドメイン] → [API アクティブ] でドメインごとに有効にします。承認は永続的で、90 日間の有効期限があり、すぐに取り消すことができます。 scan.active_api.first_used Webhook と組み合わせて、有効化後の最初の自動アクティブ スキャンがアラートに届くようにします。

調査結果を AI ワークフローに組み込む

On Hobby or above, create an API token at Account → API tokens and configure the MCP server (/docs/mcp) in your coding tool. Ask your agent to run an authorized scan and inspect the highest-severity findings. Code fixes can use remediation prompts; provider and DNS fixes may need manual operator steps.

ライブ脅威検出 (Unlimited)

Periodic certificate-transparency, DNS, JS-bundle, and threat-intelligence checks report observed changes on supported signals. Alerts depend on successful polling and source availability; they do not establish continuous or complete security coverage.

実際の失敗パターンとその修正

Five common patterns in AI-generated apps, each with the actual fix:

  1. クライアントコンポーネントのサービスロールキー

    Symptom: FixVibe reports an exposed Supabase service-role key on the production URL. Cause: an autocomplete pasted createClient(URL, SERVICE_ROLE_KEY) into a React component. Fix: move the service client to src/lib/supabase/service.ts with import 'server-only' at the top; create a parallel src/lib/supabase/client.ts using the anon key for client-side use; rotate the service-role key via Supabase Studio.

  2. Firestore ルールはテストモードのまま

    Symptom: a high-severity open Firebase rules finding. Cause: generated rules read allow read, write: if request.time < timestamp.date(2026, 6, 1); — a time-bounded "allow all". Fix: scope each rule to the authenticated user — match /users/{userId}/posts/{postId} { allow read, write: if request.auth.uid == userId; } — and re-deploy firebase deploy --only firestore:rules.

  3. 寛容な CORS が本番環境でも存続

    Symptom: a high-severity CORS misconfiguration finding. Cause: generated Express middleware: app.use(cors({ origin: '*' })). Fix: allowlist your frontend origin: app.use(cors({ origin: ['https://your-app.com'], credentials: true })). For Next.js API routes, set Access-Control-Allow-Origin explicitly in the response.

  4. RLS は有効ですが強制されていません

    Symptom: FixVibe reports that anonymous visitors can read a public table even though RLS looks enabled in the dashboard. Cause: RLS is on, but a policy such as USING (true) lets the anon role through, or the migration that tightened it never ran in production. Fix: replace the permissive policy with one scoped to auth.uid(), apply the migration, and re-scan.

  5. 署名されていない IDOR-ウォーク可能な ID

    Symptom: an active scan on your verified domain reports that one user can read another user's records at /api/items/1, /api/items/2, ... Cause: the API handler trusts the path param and queries Postgres without an ownership predicate. Fix: add .eq('user_id', user.id) on every read query, or move to signed URLs / UUIDs scoped under /api/users/[uid]/items/[id].

バイブコードセキュリティループ

目標は完璧なセキュリティではありません。これにより、AI ツールが常に見逃してしまう簡単な成果がなくなり、迅速な出荷を続けることができます。

  1. Generate fast — Cursor、Claude Code、Lovable、Bolt を使用します。それがポイントです。
  2. Audit immediately — 上記の grep セットを実行し、RLS を確認し、CSP を確認し、認証境界を確認します。
  3. Harden at deploy — ミドルウェア、環境分離、CSP nonce、HSTS、サーバーのみの認証検証。
  4. Monitor — FixVibe は毎日パッシブ、検証済みドメインで毎週アクティブ、Slack への Webhook、Unlimited で脅威検出。
  5. Fix fast — use FixVibe coding-agent prompts for code/config findings and operator steps for DNS, provider, secret-rotation, or manual-review findings. Re-deploy, re-scan, close the loop.

次のステップ

DAST と SAST の概念的な背景と、AI- で生成されたアプリに独自のスキャンが必要な理由については、AI-generated code security scanning を参照してください。出荷前監査のクイックリファレンスについては、vibe coding security checklist を参照してください。

// scan your app

読むのは終わりにして、自分のアプリの穴を見つけよう。

Drop in a URL — FixVibe runs every passive check from this guide plus the rest of its 230+ passive checks, usually in under a minute. Free, no install, no card.

  • Free 層 — 月あたり 3 回のスキャン、カードなし。
  • あらゆる URL に対するパッシブ スキャン - ドメイン検証は必要ありません。
  • Cursor、Claude Code、Lovable、Bolt、v0、Replit 用に調整されています。
  • Coding-agent prompts for code/config findings, plus operator steps for DNS/provider fixes.
無料スキャンを実行 →

サインアップ不要

AI コーディングツールで作ったアプリの守り方 · FixVibe