О насБлогКонтакты
Разработка18 апреля 2026 г. 5 мин 2

OWASP Top 10 2025: безопасность веб-приложений для казахстанского разработчика

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

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 типобезопасность

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

Node.js vs Bun vs Deno 2026: бенчмарки и выбор runtime для продакшнaunimeda
Разработка

Node.js vs Bun vs Deno 2026: бенчмарки и выбор runtime для продакшн

Bun 1.x стабилен в production. Deno 2.0 поддерживает npm-пакеты. Node.js 22 запускает TypeScript нативно. Реальные бенчмарки производительности, сравнение инструментов и конкретные рекомендации для казахстанских разработчиков.

Supabase vs Firebase 2026: сравнение для казахстанских стартапов и командaunimeda
Разработка

Supabase vs Firebase 2026: сравнение для казахстанских стартапов и команд

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

Чистая архитектура и DDD в Node.js: практическое руководство для productionaunimeda
Разработка

Чистая архитектура и DDD в Node.js: практическое руководство для production

Clean Architecture + Domain-Driven Design в Node.js TypeScript — без академизма. Use cases, Domain Entities, Repository Pattern, Aggregate Root. Бизнес-логика изолирована от инфраструктуры — тестируется без базы данных. Рабочий код для production.

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

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

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