// docs / security guides / hardening
Как защитить приложение, построенное с помощью AI-инструментов кодирования
Пошаговое руководство по усилению защиты приложений, созданных вами с помощью Cursor, Claude Code, Lovable, Bolt, v0, Replit или Windsurf. Четыре этапа: понять, почему приложения, созданные AI-, выходят из строя по-разному, провести немедленный аудит кодовой базы, усилить защиту во время развертывания, а затем продолжить мониторинг. Умный, повествовательный, с реальными фрагментами, которые можно копировать.
Почему приложения, созданные AI-, выходят из строя по-разному
Приложения с кодом Vibe могут быть безопасными. Им нужен дополнительный проход аудита, потому что виды сбоев являются структурными, а не небрежными:
- Cursor inlines hardcoded keys. Вы просите Cursor «исправить ошибку аутентификации», и он вставляет пример Supabase, в котором предполагается клиент с ролью службы. Ключ оказывается в верхней части компонента страницы. И анонимный клиент, и клиент службы сосуществуют; оба отправляются.
- Claude Code defaults to permissive CORS. Сгенерированные обработчики Express/Fastify поставляются с
cors({ origin: '*' }), потому что это самый быстрый способ получить работающую предварительную версию. Промежуточное программное обеспечение никогда не проходит второй проход. - Lovable and v0 skip the rules file. Проекты, поддерживаемые Firestore, генерируют модель данных, но редко затрагивают
firestore.rules. Правила тестового режима истекают автоматически и блокируют базу данных без предупреждения пользователя. - Bolt skips RLS migrations. Bolt генерирует схему Supabase и поверхность CRUD, использующую ключ anon.
ENABLE ROW LEVEL SECURITYникогда не участвует в миграции. Анонимные пользователи могут читать и писать любую строку. - Windsurf trusts unsigned IDs. Сгенерированный
GET /api/items/[id]считывает параметр и запрашивает Postgres без проверки владения. Паттерн настолько распространен, что активный зонд active.idor-walking обнаруживает его за одно сканирование.
Немедленный аудит: просмотрите свою кодовую базу на наличие шаблонов риска
Прежде чем что-либо укреплять, найдите то, что уже сломано. Каждая из этих операций занимает менее минуты:
Секреты и ключи провайдера
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.
Контроль доступа к базе данных
# 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 и хотя бы одна политика. Правила Firestore должны ограничивать чтение request.auth.uid.
Аутентификация и обработка сеанса
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 доступны любому скрипту, который выполняется на странице.
Заголовки и промежуточное программное обеспечение
# 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 / auth-refresh никогда не запускается.
Усиление во время развертывания
Как только исходный код будет очищен, заблокируйте путь приложения к рабочей версии.
Шаг 1. Разделение сред
Vercel: три среды — Production (ваш домен продукта), Предварительная версия (PR / промежуточные развертывания), Разработка (локальная). Каждый получает свой собственный набор env-var. Живые ключи Stripe / Anthropic / Supabase никогда не доходят до предварительного просмотра; Ключи предварительного просмотра никогда не достигают Production. Ветки автоматически переходят в режим предварительного просмотра; слияние с main развертывается с Production.
Шаг 2: Строгое CSP через промежуточное программное обеспечение
Сгенерируйте одноразовый номер для каждого запроса, а затем внедрите его в Content-Security-Policy. Next.js автоматически применяет nonce к своим собственным тегам сценария, когда вы устанавливаете заголовок запроса x-nonce.
// 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 не включен по умолчанию и не применяется для владельцев таблиц, если вы не FORCE его. Соедините каждую таблицу с явными политиками для каждой роли.
-- 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 — никогда не доверяйте его установку телу запроса.
// 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. Обратное проксирование вашей аналитики
Proxying аналитика через ваш собственный домен позволяет избежать блокировки рекламы и позволяет вашему CSP connect-src 'self' оставаться узким. Тот же шаблон работает для PostHog, Plausible, Umami, пользовательских приемников событий.
// 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. Отклоняйте все, что не является путем к тому же сайту — начните с / и никогда не // (относительно протокола, отправляет пользователей за пределы сайта).
function safeNext(raw: string | null): string {
if (!raw) return '/dashboard';
if (!raw.startsWith('/') || raw.startsWith('//')) return '/dashboard';
return raw;
}Продолжается: мониторинг и повторное сканирование
Дрифт происходит при каждом развертывании. Относитесь к безопасности как к циклу, а не как к контрольному списку, который вы выполняете.
Подтвердите свой рабочий домен
Dashboard → Domains → добавьте свой производственный домен → DNS TXT или HTTP- проверка файла (одноэтапная). После проверки активные сканирования становятся доступными, и можно включить запланированное повторное сканирование.
Запланируйте пассивное повторное сканирование
Ежедневно Hobby, 3 часа Pro, ежечасно Unlimited. Каждый запуск отправляет вам электронное письмо, если появляется новое открытие, и запускает веб-перехватчик scan.completed, если вы подписались.
# 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, чтобы первое автоматическое активное сканирование после включения достигло вашего оповещения.
Включите полученные результаты в свой рабочий процесс AI
Mint an API token at Account → API tokens, then configure the MCP server (/docs/mcp) in Claude Desktop / Cursor / Continue. Ask your agent: "Run a scan on staging and show me the highest-severity findings." The agent calls FixVibe, fetches the report, and renders categorized remediation guidance so code/config fixes become prompts and DNS/provider/manual fixes become operator steps.
Обнаружение угроз в реальном времени (Unlimited)
В журнале прозрачности сертификатов выявляются новые TLS сертификаты, выданные для вашего домена. DNS-diff-записи фиксируют несанкционированные изменения. Мониторинг секретности JS-bundle срабатывает в тот момент, когда новый ключ достигает отправленного пакета. Каналы Threat-intel (Spamhaus, URLhaus) сообщают о вашем домене, если он есть в списке.
Реальные модели сбоев и их исправления
Пять шаблонов, полученных при сканировании тысяч приложений, созданных AI-, каждый из которых имеет фактическое исправление:
- Ключ роли службы в клиентском компоненте
Symptom:
baas.supabase-service-keyнаходка на производстве URL. Cause: автозаполнение Cursor, вставленноеcreateClient(URL, SERVICE_ROLE_KEY)в компонент React. Fix: переместите клиент службы вsrc/lib/supabase/service.tsсimport 'server-only'вверху; создайте параллельныйsrc/lib/supabase/client.ts, используя ключ anon для использования на стороне клиента; поверните ключ сервисной роли через Supabase Studio. - Правила Firestore остались в тестовом режиме
Symptom:
baas.firebase-rulesвывод высокой степени тяжести. Cause: сгенерированные правила читаются какallow read, write: if request.time < timestamp.date(2026, 6, 1);— ограниченное по времени «разрешить все». Fix: распространите каждое правило на аутентифицированного пользователя —match /users/{userId}/posts/{postId} { allow read, write: if request.auth.uid == userId; }— и повторно развернитеfirebase deploy --only firestore:rules. - Разрешительное CORS выживание в производстве
Symptom:
active.corsвысокая степень серьезности. Cause: создал промежуточное программное обеспечение Express:app.use(cors({ origin: '*' })). Fix: внесите в список разрешенных источник вашего интерфейса:app.use(cors({ origin: ['https://your-app.com'], credentials: true })). Для маршрутов Next.js API укажитеAccess-Control-Allow-Originявно в ответе. - RLS включено, но не принудительно
Symptom: active
baas.supabase-rlsсообщает, что роль анона может писать в общедоступную таблицу, даже если RLS включена на панели управления. Cause:ENABLEбезFORCEоставляет владельца таблицы освобожденным — и миграция выполняется от имени владельца. Fix: добавьтеalter table public.items force row level security;к миграции. Повторное развертывание. - Неподписанные IDOR-проходимые идентификаторы
Symptom:
active.idor-walkingсообщает, что анонимный пользователь может читать/api/items/1,/api/items/2, ... среди арендаторов. Cause: обработчик API доверяет параметру пути и запрашивает Postgres без предиката владения. Fix: добавляйте.eq('user_id', user.id)в каждый запрос на чтение или переходите к подписанным URL-адресам/UUID, указанным в области/api/users/[uid]/items/[id].
Цикл безопасности с помощью vibe-кода
Целью не является идеальная безопасность; это устраняет низко висящие плоды, которые инструменты AI постоянно упускают из виду, поэтому вы можете продолжать доставку быстро.
- Generate fast — используйте Cursor, Claude Code, Lovable, Bolt. В этом вся суть.
- Audit immediately — запустите указанный выше набор grep, проверьте RLS, проверьте CSP, просмотрите границу аутентификации.
- Harden at deploy — промежуточное программное обеспечение, разделение среды, CSP nonce, HSTS, проверка подлинности только на сервере.
- Monitor — FixVibe пассивно ежедневно, активно еженедельно на проверенном домене, вебхуки на Slack, обнаружение угроз на Unlimited.
- 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.
