Урок 06

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 для защиты
  • Заголовки безопасности