CORS и безопасность
Что такое CORS
Браузер блокирует запросы к другому домену, если сервер не разрешил. CORS — набор заголовков для разрешения.
Простой CORS
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
};
export async function OPTIONS() {
return new Response(null, { status: 204, headers });
}
export async function GET() {
const data = await db.post.findMany();
return Response.json(data, { headers });
}
* — любой домен. Для продакшена — конкретные.
Ограниченный origin
const ALLOWED_ORIGINS = ['https://app.example.com', 'https://admin.example.com'];
function getCorsHeaders(origin: string | null) {
const allowed = origin && ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
return {
'Access-Control-Allow-Origin': allowed,
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
'Vary': 'Origin',
};
}
export async function GET(request: Request) {
const origin = request.headers.get('origin');
return Response.json({ ok: true }, { headers: getCorsHeaders(origin) });
}
Preflight
Браузер отправляет OPTIONS перед сложными запросами:
export async function OPTIONS(request: Request) {
const origin = request.headers.get('origin');
return new Response(null, { status: 204, headers: getCorsHeaders(origin) });
}
Credentials
Для кук и авторизации:
const headers = {
'Access-Control-Allow-Origin': 'https://app.example.com',
'Access-Control-Allow-Credentials': 'true',
};
Нельзя использовать * с credentials.
Безопасность
Валидация входных данных
const schema = z.object({
title: z.string().min(3).max(200),
body: z.string().min(10),
});
const result = schema.safeParse(await request.json());
if (!result.success) {
return Response.json({ errors: result.error.flatten() }, { status: 422 });
}
Проверка авторизации
const user = await getCurrentUser();
if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 });
Проверка прав
if (post.authorId !== user.id) {
return Response.json({ error: 'Forbidden' }, { status: 403 });
}
SQL-инъекции
ORM (Prisma, Drizzle) защищает. Для raw SQL — параметризованные запросы:
// Плохо
await db.$queryRaw`SELECT * FROM users WHERE email = '${email}'`;
// Хорошо
await db.$queryRaw`SELECT * FROM users WHERE email = ${email}`;
XSS
Не вставляй пользовательские данные через innerHTML. React экранирует автоматически.
CSRF
Для cookie-based auth — проверяй Origin или используй SameSite=Strict:
const origin = request.headers.get('origin');
if (origin !== process.env.APP_URL) {
return Response.json({ error: 'Invalid origin' }, { status: 403 });
}
Rate limiting
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'),
});
export async function POST(request: Request) {
const ip = request.headers.get('x-forwarded-for') ?? 'unknown';
const { success } = await ratelimit.limit(ip);
if (!success) {
return Response.json({ error: 'Too many requests' }, { status: 429 });
}
// обработка
}
Заголовки безопасности
const response = Response.json(data);
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
return response;
Или через next.config.ts:
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
],
},
];
},
};
Итоги
- CORS через заголовки
OPTIONSдля preflight- Credentials требуют конкретный origin
- Валидация всех входных данных
- Проверка авторизации и прав
- Rate limiting для защиты
- Заголовки безопасности