OWASP Top 10 2025: безопасность веб-приложений для казахстанского разработчика
Большинство утечек данных происходят через уязвимости из OWASP Top 10. Это не абстрактный список — это конкретные паттерны кода, которые позволяют атакующему обойти вашу систему. Для разработчиков, строящих приложения для казахстанского рынка, безопасность — это и техническое требование, и юридическое (ЗРК «О персональных данных»).
A01: Сломанный контроль доступа
Четвёртый год на первом месте. Атакующий получает доступ к чужим ресурсам.
// ❌ Проверяет только факт авторизации, не владельца
router.get('/api/orders/:orderId', authenticate, async (req, res) => {
const order = await db.order.findUnique({ where: { id: req.params.orderId } });
res.json(order);
});
// ✅ Скоуп на текущего пользователя
router.get('/api/orders/:orderId', authenticate, async (req, res) => {
const order = await db.order.findUnique({
where: {
id: req.params.orderId,
userId: req.user.id,
},
});
if (!order) return res.status(404).json({ error: 'Заказ не найден' });
res.json(order);
});
// ✅ Явная проверка роли для admin
function requireRole(role: string) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user.roles.includes(role)) return res.status(403).json({ error: 'Доступ запрещён' });
next();
};
}
A02: Криптографические сбои
// ❌ MD5 — не алгоритм хэширования паролей
const hash = crypto.createHash('md5').update(password).digest('hex');
// ✅ bcrypt с cost factor 12
import bcrypt from 'bcrypt';
async function hashPassword(password: string) {
return bcrypt.hash(password, 12);
}
async function verifyPassword(password: string, hash: string) {
return bcrypt.compare(password, hash);
}
// ✅ Криптографически безопасный токен
const token = crypto.randomBytes(32).toString('hex');
const hash = crypto.createHash('sha256').update(token).digest('hex');
// Храним хэш в БД, токен отправляем пользователю
// ✅ JWT с коротким сроком жизни
import { SignJWT } from 'jose';
const token = await new SignJWT({ userId, role })
.setProtectedHeader({ alg: 'RS256' })
.setExpirationTime('15m') // 15 минут
.sign(privateKey);
A03: Инъекции
// ❌ SQL-инъекция
const user = await db.query(
`SELECT * FROM users WHERE email = '${req.body.email}'`
);
// email = "admin'--" → вход без пароля
// ✅ Параметризованный запрос
const user = await db.query('SELECT * FROM users WHERE email = $1', [req.body.email]);
// ✅ ORM — параметризация по умолчанию
const user = await prisma.user.findUnique({ where: { email: req.body.email } });
// NoSQL-инъекция (MongoDB)
// ❌ Уязвимо
const user = await User.findOne({ email: req.body.email });
// Атака: { "email": { "$gt": "" } } → обход авторизации
// ✅ Валидация схемой
import { z } from 'zod';
const { email } = z.object({ email: z.string().email() }).parse(req.body);
A05: Некорректная конфигурация
Обязательные заголовки безопасности:
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'", 'https://api.aunimeda.com'],
},
},
hsts: {
maxAge: 63072000,
includeSubDomains: true,
preload: true,
},
}));
// Никаких stack trace в production
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
logger.error({ err, url: req.url, userId: req.user?.id });
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
});
A07: Сбои аутентификации
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
// Rate limiting — защита от брутфорса
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
store: new RedisStore({ client: redis }),
handler: (req, res) => {
res.status(429).json({ error: 'Слишком много попыток. Попробуйте через 15 минут.' });
},
});
app.post('/api/auth/login', loginLimiter, loginHandler);
// Не раскрывать существование аккаунта
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
return res.status(401).json({ error: 'Неверный email или пароль' });
// Одинаковый ответ — нельзя определить существует ли email
}
A06: Устаревшие компоненты с уязвимостями
# Регулярный аудит
npm audit
# В CI — блокировать при высоких уязвимостях
npx audit-ci --high
# GitHub Actions
- name: Security Audit
run: npm audit --audit-level=moderate
- name: Trivy Vulnerability Scan
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
severity: 'HIGH,CRITICAL'
exit-code: '1'
A10: SSRF
// ❌ Атакующий может запросить внутренние сервисы
app.post('/api/webhook-test', async (req, res) => {
const response = await fetch(req.body.url);
// http://169.254.169.254/ → утечка AWS IAM credentials
});
// ✅ Проверяем что URL не ведёт во внутреннюю сеть
import dns from 'dns/promises';
import ipRangeCheck from 'ip-range-check';
const PRIVATE_RANGES = [
'10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16',
'127.0.0.0/8', '169.254.0.0/16', '::1/128',
];
async function isSafeUrl(urlString: string): Promise<boolean> {
try {
const url = new URL(urlString);
if (!['http:', 'https:'].includes(url.protocol)) return false;
const ips = await dns.resolve4(url.hostname);
return ips.every(ip => !ipRangeCheck(ip, PRIVATE_RANGES));
} catch { return false; }
}
Логирование событий безопасности
Важно для соответствия требованиям ЗРК «Об информационной безопасности»:
const securityLog = {
authFailure: (email: string, ip: string) => logger.warn({
event: 'AUTH_FAILURE', email, ip, ts: Date.now(),
}),
accessDenied: (userId: string, resource: string) => logger.warn({
event: 'ACCESS_DENIED', userId, resource, ts: Date.now(),
}),
suspiciousActivity: (userId: string, details: object) => logger.warn({
event: 'SUSPICIOUS', userId, details, ts: Date.now(),
}),
};
Чеклист безопасности
[ ] Zod-валидация всего пользовательского ввода
[ ] Параметризованные SQL-запросы везде
[ ] bcrypt/argon2 для паролей (cost factor 10+)
[ ] Helmet с CSP
[ ] Rate limiting на /login, /register, /password-reset
[ ] JWT: 15 минут + refresh token rotation
[ ] npm audit в CI
[ ] Stack trace недоступен в production
[ ] Каждый ресурс проверяет владельца
[ ] HTTPS + HSTS + preload
[ ] Логирование auth failures и access denied
Aunimeda строит защищённые веб-приложения для казахстанского рынка. Обсудим проект.
Смотрите также: TanStack Query production паттерны, tRPC + Zod типобезопасность