OWASP Top 10: безопасность веб-приложений — реальные уязвимости и защита
OWASP Top 10 — стандарт классификации рисков безопасности для веб-приложений. За каждым пунктом — конкретные атаки, которые происходят с реальными приложениями. Этот разбор — для разработчиков, которые строят веб-сервисы в Кыргызстане и хотят их защитить.
A01: Сломанный контроль доступа
Самая распространённая уязвимость уже несколько лет подряд.
Атака IDOR:
GET /api/users/my-id/orders ← мои заказы
GET /api/users/victim-id/orders ← чужие заказы (та же структура!)
// ❌ Проверяет только что пользователь залогинен
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 или SHA1 для паролей:
// ❌ Небезопасно — MD5/SHA1 брутфорсятся за минуты
const hash = crypto.createHash('md5').update(password).digest('hex');
// ✅ bcrypt — намеренно медленный алгоритм
import bcrypt from 'bcrypt';
async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, 12); // cost factor 12 ≈ 250мс
}
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
// ✅ Безопасный случайный токен для сброса пароля
import crypto from 'crypto';
const token = crypto.randomBytes(32).toString('hex');
A03: Инъекции (SQL, NoSQL, команды ОС)
// ❌ 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):
// ❌ Уязвимо
// Атака: { "email": {"$gt": ""}, "password": {"$gt": ""} } → обходит авторизацию
const user = await User.findOne({ email: req.body.email, password: req.body.password });
// ✅ Валидируем входные данные перед запросом
import { z } from 'zod';
const LoginSchema = z.object({
email: z.string().email(),
password: z.string().min(1),
});
const { email, password } = LoginSchema.parse(req.body);
A05: Некорректная конфигурация безопасности
Обязательные HTTP-заголовки для любого веб-приложения:
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
hsts: {
maxAge: 63072000, // 2 года
includeSubDomains: true,
},
}));
// Никаких stack trace в production
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
logger.error({ err, url: req.url });
res.status(500).json(
process.env.NODE_ENV === 'production'
? { error: 'Внутренняя ошибка' }
: { error: err.message, stack: err.stack }
);
});
A07: Сбои аутентификации
Rate limiting — защита от брутфорса:
import rateLimit from 'express-rate-limit';
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 минут
max: 10,
handler: (req, res) => {
res.status(429).json({ error: 'Слишком много попыток. Подождите 15 минут.' });
},
});
app.post('/api/auth/login', loginLimiter, loginHandler);
// Не раскрывайте существование email
// ❌ Помогает атакующему перебирать базу пользователей
if (!user) return res.status(404).json({ error: 'Пользователь не найден' });
// ✅ Одинаковый ответ при любом исходе
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
return res.status(401).json({ error: 'Неверный email или пароль' });
}
A10: SSRF — подделка серверных запросов
// ❌ Уязвимо
app.post('/api/fetch-preview', async (req, res) => {
const response = await fetch(req.body.url);
// Атакующий передаёт http://169.254.169.254/ → утечка AWS credentials
});
// ✅ Блокируем внутренние IP-диапазоны
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'];
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;
}
}
Минимальный чеклист безопасности
[ ] Весь ввод пользователя валидируется (Zod)
[ ] Только параметризованные SQL-запросы
[ ] Пароли: bcrypt с cost factor 10+
[ ] Security headers: Helmet
[ ] Rate limiting на /login и /register
[ ] JWT: срок жизни 15 минут + refresh token
[ ] npm audit в CI/CD пайплайне
[ ] Нет stack trace в production
[ ] Каждый запрос к ресурсу проверяет владельца
[ ] HTTPS + HSTS + preload
Aunimeda строит защищённые веб-приложения для бизнеса в Кыргызстане. Обсудим проект.
Смотрите также: Next.js 15 Server Components, Telegram Bot FSM и middleware