В нашу отрасль просочилась опасная мысль: будто безопасность — чья-то чужая проблема. Дело команды эксплуатации, инженеров по безопасности, специалистов, которые раз в квартал приходят потыкать в приложение. Эта мысль неверна, и именно она стоит за большинством утечек, о которых вы читаете по вторникам.
На деле безопасность решается не на ревью безопасности, а в редакторе. Каждое ваше решение — как собран SQL-запрос, как обработан пользовательский ввод, где хранится токен сессии — либо открывает дверь, либо надёжно её закрывает. Атакующему хватит одной открытой. Вам нужно закрыть все.
В этой статье — практики безопасного кодирования, которые стоит знать каждому разработчику. За основу взят OWASP Top 10 2026 года и те слабости, которые встречаются в реальных кодовых базах каждый день: инъекции, межсайтовый скриптинг, подделка межсайтовых запросов, изъяны аутентификации, риски зависимостей, утечки секретов. В каждом разделе есть конкретные приёмы, применимые уже сегодня.
OWASP Top 10 2026 года: что изменилось и почему
OWASP Top 10 — ближайшее к отраслевому консенсусу перечисление главных рисков веб-приложений. В версии 2026 года появилось несколько заметных изменений, отражающих то, как эволюционировал ландшафт угроз. Понимание этих сдвигов помогает сосредоточиться на том, что действительно весомо.
Главное изменение 2026 года — выделение кода, сгенерированного ИИ, в отдельную категорию риска. OWASP впервые прямо признал, что код, выданный большими языковыми моделями, несёт собственные проблемы безопасности. Эти модели обучены на огромных корпусах публичного кода — от хорошо поддерживаемых библиотек до фрагментов, скопированных без понимания. ИИ воспроизводит увиденные шаблоны, в том числе опасные. Исследование Snyk 2025 года показало, что при запросах, неоднозначных с точки зрения безопасности, ассистент примерно в 30% случаев предлагал код с известными уязвимостями. Вывод ясен: сгенерированный ИИ код требует того же уровня проверки, что и код от незнакомого подрядчика.
Второе значимое изменение — объединение категорий инъекций. Раньше SQL-инъекции, NoSQL-инъекции, инъекции команд ОС и LDAP считались отдельными пунктами. Версия 2026 года сводит их в одну категорию «Инъекции». Это отражает реальность: базовый шаблон — конкатенация недоверенных данных в строку для интерпретатора — одинаков независимо от цели. Полезная переформулировка, переводящая внимание с частных вариантов на первопричину.
Криптографические сбои поднялись в списке. Причины — распространение криптоанализа с помощью ИИ и отставание в планах перехода к постквантовым алгоритмам. Если вы не используете современное аутентифицированное шифрование (AES-GCM, ChaCha20-Poly1305) и даже не начали думать о постквантовом переходе для долго хранящихся данных, вы копите долг, который вернётся с процентами.
Версия 2026 года также объединяет ошибки конфигурации безопасности и нарушения целостности ПО в широкую категорию «конфигурация и компрометация». Это признание того, что многие нарушения целостности — например, использование недоверенных базовых образов или неподписанных зависимостей — суть решения, принятые на входе в цикл разработки.
Безопасность — не продукт и не функция. Это свойство инженерной культуры. Если вы не думаете о безопасности на этапе проектирования, никакое сканирование в конце этого не исправит.
Инъекции: старая история, которая всё ещё возглавляет список
Инъекции держатся на вершине списка OWASP не случайно: их легко провести, последствия тяжёлые, а встречаются они в продакшен-коде удивительно часто. Корень проблемы в том, что программа собирает команду или запрос конкатенацией строк, часть которых контролирует пользователь. Атакующий подбирает ввод так, чтобы выйти из ожидаемого контекста и вставить собственные инструкции.
SQL-инъекция — классический пример, который до сих пор работает. Разработчик, изучавший её в университете десять лет назад, под давлением срока всё равно пишет опасный код. Решение хорошо известно: параметризованные запросы, подготовленные выражения или ORM, который корректно экранирует значения.
// Vulnerable: string concatenation with user input
const query = `SELECT * FROM users WHERE email = '${req.query.email}'`;
db.execute(query);
// Fixed: parameterized query
const query = 'SELECT * FROM users WHERE email = ?';
db.execute(query, [req.query.email]);Опасный шаблон опытный разработчик распознаёт сразу, и всё же он появляется на ревью каждую неделю. Причины всегда одни: нет времени, запрос кажется слишком простым для параметризации, ORM не поддерживает нужный драйвер. Эти причины не переживают разбор инцидента.
Тот же принцип применим к базам NoSQL. MongoDB, например, уязвим для инъекций, когда пользовательский ввод напрямую попадает в объект запроса. Передав в качестве пароля { '$ne': '' }, атакующий полностью обходит аутентификацию, если запрос собирается из сырого ввода.
Инъекция команд ОС — ещё одна разновидность, которая никак не умрёт. Вызов exec, spawn или порождение дочернего процесса с пользовательским вводом — худший из вариантов. Если внешнюю команду выполнить необходимо, проверяйте ввод по строгому списку разрешённых значений. Никогда не собирайте строку команды из пользовательского ввода.
Межсайтовый скриптинг и особенности SPA
Межсайтовый скриптинг (XSS) — инъекция, нацеленная не на базу данных, а на DOM браузера. Атакующий внедряет вредоносный JavaScript в страницу, и скрипт выполняется в контексте сессии жертвы, получая доступ к кукам, localStorage и данным сессии, а заодно возможность отправлять запросы от её имени.
Появление одностраничных приложений на React, Vue и Svelte сильно изменило картину XSS. Современные фреймворки автоматически экранируют значения в шаблонных выражениях и закрывают самый частый вектор. Но разработчик по-прежнему может обойти защиту: dangerouslySetInnerHTML в React, v-html во Vue, @html в Svelte. У этих API есть законные применения (например, отрисовка форматированного текста из CMS), но без предварительной санитизации они открывают дверь для XSS.
// Vulnerable: rendering unsanitized HTML from user input
function Comment({ body }) {
return <div dangerouslySetInnerHTML={{ __html: body }} />;
}
// Fixed: sanitize before rendering
import DOMPurify from 'dompurify';
function Comment({ body }) {
const clean = DOMPurify.sanitize(body);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}Серверный рендеринг добавляет ещё одно измерение. Если приложение отрисовывает пользовательский контент на сервере и отправляет HTML клиенту, экранировать нужно на сервере. Многие шаблонизаторы по умолчанию выдают экранированный вывод, но позволяют выбрать неэкранированный через специальный синтаксис. Просмотрите все места, где экранирование отключено.
Третий вектор, который часто упускают, — косвенная инъекция через data-атрибуты и URL. Если вы задаёте data-атрибут элемента из пользовательского ввода и ввод содержит символы, разрывающие контекст атрибута, это опасно. Так же и с URL: если вы собираете ссылку из пользовательского ввода и вставляете её в тег anchor, атакующий может выполнить произвольный код через URL со схемой javascript:.
Подделка межсайтовых запросов и укрепление аутентификации
Подделка межсайтовых запросов (CSRF) злоупотребляет доверием приложения к браузеру аутентифицированного пользователя. Атакующий формирует запрос к вашему приложению и заставляет пользователя его выполнить — через посещение вредоносной страницы, клик по ссылке или загрузку картинки. Если аутентификация держится только на куке сессии, браузер автоматически приложит её к любому запросу на ваш домен, и атака сработает.
Стандартная защита — CSRF-токен: уникальное, трудно угадываемое значение, встроенное в каждую форму и проверяемое на сервере. При отправке формы сервер сверяет токен с сохранённым в сессии. Подделанный запрос не может содержать этот токен, потому что политика одного источника не даёт странице атакующего прочитать ответ вашего сервера.
// Vulnerable: no CSRF protection
app.post('/api/transfer', (req, res) => {
const { toAccount, amount } = req.body;
transferFunds(req.session.userId, toAccount, amount);
res.json({ ok: true });
});
// Fixed: validate CSRF token
app.post('/api/transfer', (req, res) => {
const token = req.headers['x-csrf-token'];
if (!validateCsrfToken(req.session, token)) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
const { toAccount, amount } = req.body;
transferFunds(req.session.userId, toAccount, amount);
res.json({ ok: true });
});Современные фреймворки дают защиту от CSRF из коробки. Ошибка, которую часто совершают разработчики, — отключить её на эндпоинтах API, не отдающих HTML-форм. Без корректной настройки CORS такие эндпоинты тоже становятся мишенью для межсайтовых запросов.
Укрепление аутентификации не сводится к CSRF. У каждой сессии должен быть срок жизни. Токен сброса пароля должен быть одноразовым и ограниченным по времени. На каждом эндпоинте входа должно быть ограничение частоты запросов, чтобы остановить перебор. Многофакторная аутентификация должна быть доступна всем пользователям, рекомендована по умолчанию и обязательна для административных учётных записей.
Ограничение частоты — одна из самых выгодных мер безопасности по соотношению затрат и эффекта. Требует минимума кода, не создаёт постоянной нагрузки на поддержку и останавливает целый класс атак: подстановку украденных учётных данных, перебор, перечисление, разнообразные злоупотребления API. Внедряйте его на уровне приложения (на пользователя), сети (на IP) и эндпоинта (на чувствительные операции). Для входа разумная отправная точка — пять попыток в минуту на пользователя.
Управление зависимостями и обращение с секретами
Современные приложения стоят на фундаменте открытых зависимостей. Типичный проект на Node.js тянет тысячи транзитивных пакетов, и каждый — потенциальный вектор атаки. Случай с event-stream в 2024 году, когда злоумышленник получил права сопровождающего популярного пакета npm и внедрил код для кражи криптовалюты, — не аномалия, а предупреждение о системном риске цепочки поставок.
- Используйте менеджер пакетов с проверкой целостности. npm shrinkwrap и yarn.lock гарантируют, что при любой установке будет собрано ровно то же дерево зависимостей. Фиксируйте версии и не используйте диапазоны.
- Запускайте сканирование уязвимостей зависимостей в составе конвейера CI. npm audit, Snyk, Dependabot, Trivy автоматически находят известные слабости и останавливают сборку при критических находках.
- Удаляйте неиспользуемые зависимости. Пакет, которым вы не пользуетесь, — долг без выгоды. Найти простаивающие помогает depcheck.
- Проверяйте, какие права требует каждая зависимость. Пакеты, читающие переменные окружения, обращающиеся к файловой системе или делающие сетевые запросы, — риск. Каждое такое право должно быть обосновано.
- Настройте автоматическое обновление зависимостей с процессом ревью. Dependabot или Renovate откроют пул-реквест при выходе новой версии. Проверьте журнал изменений, оцените ломающие изменения и вливайте быстро — особенно исправления безопасности.
Управление секретами — вторая половина уравнения. Ключи API, пароли баз данных и криптографические ключи, вписанные прямо в исходный код, — одна из самых частых находок на пентесте и при этом полностью предотвратимая.
// Vulnerable: hardcoded secrets in source code
const DB_PASSWORD = 'sup3r-s3cr3t-passw0rd!';
const API_KEY = 'sk-live-abc123def456';
// Fixed: use environment variables with validation
function getEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}
const dbPassword = getEnv('DB_PASSWORD');
const apiKey = getEnv('API_KEY');
// Also: use .env files locally, never commit them
// Add .env to .gitignore from day oneЕсли вы когда-либо коммитили секрет в репозиторий Git, считайте его скомпрометированным. Удалить его из последнего коммита недостаточно — он остаётся в истории, и найти его может любой, у кого есть доступ. Единственный безопасный ответ: немедленно сменить секрет, а если репозиторий публичный или общий, вычистить историю инструментами вроде git-filter-repo или BFG Repo-Cleaner.
В продакшене используйте специализированное хранилище секретов. HashiCorp Vault, AWS Secrets Manager, Azure Key Vault или Google Secret Manager дают безопасное хранение, аудит обращений, автоматическую ротацию и тонкое управление доступом. Для локальной разработки переменных окружения достаточно, но продакшен-секреты должны запрашиваться из хранилища при старте приложения, а не зашиваться в конфигурацию среды.
Инструменты: SAST, DAST и заголовки безопасности
Безопасное кодирование — это не только то, что вы пишете, но и то, что вы проверяете и как выкатываете. Самые дисциплинированные команды сочетают статический анализ, динамический анализ и укрепление инфраструктуры, создавая несколько слоёв защиты.
Инструменты SAST анализируют исходный код без его выполнения. Они ищут шаблоны, указывающие на уязвимости: инъекции, XSS, небезопасную десериализацию, вшитые секреты. SAST встраивается в рабочий процесс и даёт обратную связь в момент написания кода, когда исправление стоит дешевле всего. Из распространённых — Semgrep, SonarQube, CodeQL, ESLint с плагинами безопасности. Ключ к успешному внедрению — держать уровень шума низким: настраивайте правила так, чтобы каждое предупреждение вело к действию. Иначе разработчики просто научатся их игнорировать.
Инструменты DAST анализируют работающее приложение, отправляя подготовленные запросы и наблюдая ответы. Они находят то, чего не видит SAST: CSRF, обход аутентификации, проблемы конфигурации. Среди них OWASP ZAP, Burp Suite, Acunetix, StackHawk. Лучше всего DAST работает, когда встроен в конвейер CI/CD и запускается на стенде после выкладки.
Третий слой — укрепление инфраструктуры через заголовки безопасности. Заголовки HTTP-ответа задают поведение браузера при отрисовке вашего приложения. Правильно настроив их, вы предотвращаете целый класс клиентских атак без затрат во время выполнения.
- Content-Security-Policy: ограничивает источники, из которых браузер может загружать ресурсы. Предотвращает XSS даже тогда, когда атакующему удалось внедрить тег скрипта.
- Strict-Transport-Security: заставляет браузер использовать HTTPS для всех соединений с вашим доменом. Защищает от атак с понижением до HTTP.
- X-Frame-Options: не даёт встраивать ваше приложение в iframe на чужом домене. Останавливает кликджекинг.
- X-Content-Type-Options: запрещает браузеру угадывать тип содержимого. Снижает риск внедрения скрипта через неверно указанный тип.
- Set-Cookie с флагами Secure, HttpOnly и SameSite: кука уходит только по HTTPS, недоступна из JavaScript и не отправляется в межсайтовых запросах. SameSite=Lax или Strict — самая простая и действенная защита от CSRF.
Заголовки безопасности легко настроить и они дают эффект сразу. Просканируйте приложение инструментом вроде securityheaders.com и найдите недостающие. Большинство фреймворков поддерживает их установку через промежуточное ПО: Helmet в Express, middleware безопасности в Django, Rack::Protection в Rails — все хорошо поддерживаются и широко используются.
Сочетание SAST во время разработки, DAST во время выкладки и заголовков безопасности на уровне инфраструктуры создаёт перекрывающееся покрытие. Если один слой пропустит уязвимость, её может поймать другой. Это эшелонированная оборона, наложенная на жизненный цикл разработки.
Как строить культуру безопасности
Никакие инструменты, чек-листы и методологии не заменят команду, для которой безопасность — внутреннее проектное ограничение. Разница между командами, пишущими безопасный код, и остальными — не в качестве статического анализатора. Это набор привычек и допущений, встроенных в ежедневную работу.
Самая важная привычка — моделирование угроз до реализации. Прежде чем писать хоть строку новой функции, спросите: что будет пытаться сделать атакующий? Какие самые ценные данные затрагивает эта функция? Что произойдёт, если атакующий получит контроль над вводом? Что произойдёт, если база данных окажется скомпрометирована? Ответы на эти вопросы на раннем этапе вскрывают уязвимости уровня архитектуры, которые ревью кода поймать не в состоянии, — потому что они лежат не в реализации, а в проекте.
Вторая привычка — ревью, где безопасность стоит в одном ряду с остальным. Ревью, сосредоточенное только на стиле, корректности и производительности, пропускает проблемы безопасности. Добавьте в шаблон пул-реквеста пункты о безопасности: SQL-инъекции? XSS? CSRF? Есть ли секреты в диффе? Проверка прав? Ограничение частоты? Так ответственность распределяется по команде, а проблемы находятся до продакшена.
Третья привычка — учиться на инцидентах. Когда уязвимость найдена — в вашей кодовой базе или чужой, — отнеситесь к ней как к учебному материалу. Проведите разбор, сфокусированный на системных причинах: почему уязвимость появилась? почему её не поймали на ревью? какое изменение процесса предотвратит этот класс проблем в будущем? Разбор без поиска виноватых строит культуру безопасности, разбор с поиском виноватых её разрушает.
Безопасное кодирование — не навык, который осваивают раз и навсегда. Ландшафт угроз меняется. Инструменты меняются. Ваше понимание меняется. Каждому разработчику стоит раз в полгода вкладывать время в изучение новых векторов атак, новых приёмов защиты и новых инструментов. Читайте памятки OWASP. Следите за исследователями безопасности в источниках, которым доверяете. Прогоняйте пентест против собственного приложения. Цель не в том, чтобы достичь полной безопасности — это невозможно. Цель в том, чтобы сделать ваше приложение более крепкой мишенью, чем следующая, и вынудить атакующего пройти мимо.
Потому что в конечном счёте дело именно в этом. Безопасность — не создание системы, которую нельзя взломать. Это создание системы, которую не стоит взламывать. Каждая закрытая вами уязвимость, каждый вынесенный из кода секрет, каждое внедрённое ограничение частоты делают более привлекательной мишенью кого-то другого. Не будьте самым низко висящим плодом.