Урок 08

Безопасность продакшена

Заголовки безопасности

В next.config.ts:

const nextConfig = {
    async headers() {
        return [
            {
                source: '/(.*)',
                headers: [
                    { key: 'X-Content-Type-Options', value: 'nosniff' },
                    { key: 'X-Frame-Options', value: 'DENY' },
                    { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
                    { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
                    {
                        key: 'Strict-Transport-Security',
                        value: 'max-age=63072000; includeSubDomains; preload',
                    },
                ],
            },
        ];
    },
};

Content Security Policy

Защита от XSS:

{
    key: 'Content-Security-Policy',
    value: [
        "default-src 'self'",
        "script-src 'self' 'unsafe-inline' https://vercel.live",
        "style-src 'self' 'unsafe-inline'",
        "img-src 'self' data: https:",
        "font-src 'self' data:",
        "connect-src 'self' https://api.example.com",
        "frame-ancestors 'none'",
    ].join('; '),
}

CSP легко сломать. Начни с Report-Only:

{
    key: 'Content-Security-Policy-Report-Only',
    value: "...",
}

Смотри отчёты, потом включай.

CSRF

Для cookie-based auth — проверяй Origin:

export async function POST(request: Request) {
    const origin = request.headers.get('origin');

    if (origin !== process.env.APP_URL) {
        return Response.json({ error: 'Invalid origin' }, { status: 403 });
    }

    // обработка
}

Или используй SameSite=Strict:

cookieStore.set('token', value, { sameSite: 'strict' });

Cookies

cookieStore.set('token', value, {
    httpOnly: true,                                 // недоступно из JS
    secure: process.env.NODE_ENV === 'production', // только HTTPS
    sameSite: 'lax',                               // защита от CSRF
    maxAge: 60 * 60 * 24 * 7,
    path: '/',
});

SQL-инъекции

ORM защищает:

await db.post.findMany({ where: { title: search } });

Raw SQL — параметризованно:

// Плохо
await db.$queryRawUnsafe(`SELECT * FROM users WHERE email = '${email}'`);

// Хорошо
await db.$queryRaw`SELECT * FROM users WHERE email = ${email}`;

XSS

React экранирует автоматически. Не используй dangerouslySetInnerHTML с пользовательскими данными:

// Опасно
<div dangerouslySetInnerHTML={{ __html: userInput }} />

// Безопасно
<div>{userInput}</div>

Если нужно — санитизируй:

npm install dompurify isomorphic-dompurify
import DOMPurify from 'isomorphic-dompurify';

<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />

Rate limiting

npm install @upstash/ratelimit @upstash/redis
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 schema = z.object({
    email: z.string().email(),
    password: z.string().min(8),
});

const result = schema.safeParse(await request.json());
if (!result.success) {
    return Response.json({ errors: result.error.flatten() }, { status: 422 });
}

Аутентификация

  • Пароли — bcrypt или argon2
  • JWT — короткий срок + refresh
  • Сессии — httpOnly cookies
  • 2FA — для критичных действий
  • OAuth — для соцсетей
npm install bcryptjs
import bcrypt from 'bcryptjs';

const hash = await bcrypt.hash(password, 12);
const valid = await bcrypt.compare(password, hash);

Secrets

  • Не в репозитории
  • Не в клиентском бандле
  • Не в логах
  • Ротация периодически

Зависимости

npm audit
npm audit fix

Dependabot на GitHub:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: npm
    directory: /
    schedule:
      interval: weekly

Что проверить перед релизом

  • HTTPS работает
  • Cookies с HttpOnly, Secure, SameSite
  • CSP настроен
  • Rate limiting на auth-эндпоинтах
  • Валидация всех входных данных
  • Пароли хешируются
  • Нет секретов в клиенте
  • npm audit без критичных
  • Sentry ловит ошибки

Итоги

  • Заголовки безопасности
  • CSP постепенно
  • CSRF через Origin или SameSite
  • Cookies с флагами
  • Rate limiting
  • Валидация всегда
  • Обновляй зависимости