О насБлогКонтакты
Безопасность18 апреля 2026 г. 4 мин 7

OWASP Top 10: безопасность веб-приложений — реальные уязвимости и защита

AunimedaAunimeda
📋 Содержание

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

Читайте также

Мониторинг Node.js в production: Prometheus, Grafana и OpenTelemetryaunimeda
DevOps

Мониторинг Node.js в production: Prometheus, Grafana и OpenTelemetry

Как понять что ваш Node.js сервер падает ещё до того как пользователи начали жаловаться. Настройка метрик с Prometheus, дашборды в Grafana, трейсинг с OpenTelemetry — полная конфигурация для production.

WebSockets vs SSE vs Long Polling: выбор технологии realtime для вашего приложенияaunimeda
Разработка

WebSockets vs SSE vs Long Polling: выбор технологии realtime для вашего приложения

Чат, уведомления, live-обновления заказов, онлайн-счётчики — все они требуют realtime. WebSocket, Server-Sent Events и Long Polling работают по-разному. Разбираем когда каждый подход лучше, с реальным кодом на Node.js.

Supabase vs Firebase: что выбрать для стартапа в Бишкекеaunimeda
Разработка

Supabase vs Firebase: что выбрать для стартапа в Бишкеке

Supabase — open-source с PostgreSQL, самохостинг возможен. Firebase — зрелый Google BaaS. PocketBase — один бинарник для MVP. Сравниваем модели данных, цены, realtime и когда каждый вариант оправдан для бишкекских стартапов.

Нужна IT-разработка для вашего бизнеса?

Разрабатываем сайты, мобильные приложения и AI-решения для бизнеса в Кыргызстане. Бесплатная консультация.

Получить консультацию Все статьи