Безопасность продакшена
Заголовки безопасности
В 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
- Валидация всегда
- Обновляй зависимости