Security6 мин чтения2026-03-06

Ошибка безопасности в коде: как её найти и не допустить снова

Слышу часто: "У нас есть SecurityLab, проверяем код вручную". Потом выясняется, что проверяет его один человек в спешке, перед релизом, и половину уязвимостей п

Слышу часто: "У нас есть SecurityLab, проверяем код вручную". Потом выясняется, что проверяет его один человек в спешке, перед релизом, и половину уязвимостей пропускает. Потому что человек устаёт. Машина — нет.

Давайте разберёмся, что такое ошибка безопасности в коде, почему её так сложно найти вручную, и как автоматизировать эту проверку так, чтобы уязвимость не дошла до production.

Что считается ошибкой безопасности в коде

Ошибка безопасности — это фрагмент кода, который позволяет злоумышленнику выполнить несанкционированные действия. Получить доступ к данным, запустить свой код, обойти аутентификацию, стереть базу. Всё это.

Вот типичные примеры:

SQL injection. Ты берёшь пользовательский ввод и подставляешь его прямо в SQL-запрос.

# Опасно!
user_id = request.args.get('id')
query = f"SELECT * FROM users WHERE id = {user_id}"
result = db.execute(query)

Пользователь вводит вместо ID что-то вроде 1 OR 1=1 — и получает все записи из таблицы. Не круто.

XSS (Cross-Site Scripting). Ты выводишь в HTML данные от пользователя без экранирования.

<!-- Опасно! -->
<div>Привет, {{ user_input }}</div>

Если user_input содержит <script>alert('hacked')</script>, этот скрипт выполнится в браузере.

Hardcoded credentials. Пароли и API-ключи прямо в исходном коде.

API_KEY = "sk-1234567890abcdef"
DB_PASSWORD = "admin123"

Репозиторий утекает на GitHub — ключи у всех.

Path traversal. Пользователь может получить доступ к файлам вне предполагаемой директории.

# Опасно!
filename = request.args.get('file')
with open(f'/uploads/{filename}', 'r') as f:
    return f.read()

Ввод ../../etc/passwd — и у тебя проблемы.

CSRF (Cross-Site Request Forgery). Запрос выполняется от имени пользователя без его ведома.

# Опасно! Нет проверки CSRF токена
@app.post('/transfer-money')
def transfer_money():
    amount = request.form.get('amount')
    # Переводим деньги

Это самые частые. Есть ещё куча других — но 80% уязвимостей в production укладываются в эти категории.

SAST и DAST: как машина ловит ошибки безопасности

Есть два подхода к автоматизированному поиску уязвимостей.

SAST (Static Application Security Testing) анализирует код без его запуска. Инструмент смотрит на исходник и ищет паттерны опасного кода. Быстро, дёшево, ловит много ошибок на этапе разработки.

Работает вот так: ты пишешь код, делаешь коммит, SAST-инструмент парсит AST (Abstract Syntax Tree) и проверяет каждый узел против базы правил. Если нашёл опасный паттерн — выдаёт warning.

Примеры SAST-инструментов: SonarQube, Checkmarx, Semgrep, Bandit (для Python).

# Bandit для Python
bandit -r ./src/

# Найдёт hardcoded credentials, SQL injections и другое

DAST (Dynamic Application Security Testing) — это когда ты запускаешь приложение и пытаешься его сломать. Фаззинг, перебор параметров, попытки обхода аутентификации — всё это.

DAST медленнее SAST, но ловит уязвимости, которые статический анализ может пропустить. Например, логику в runtime.

Примеры: Burp Suite, OWASP ZAP, Acunetix.

По-хорошему, нужны оба. SAST — в процессе разработки (в CI/CD pipeline), DAST — перед продакшеном.

Как уязвимости попадают в production

Сценарий, который я видел на трёх разных проектах:

  1. Разработчик пишет код. Не думает о безопасности — просто решает задачу.
  2. Code review. Коллега смотрит код, проверяет логику, синтаксис. Про безопасность обычно забывает (это не его обязанность, вроде).
  3. Тесты проходят. CI/CD зелёный. Merge в master.
  4. Неделю спустя — найдена уязвимость. Или хуже — обнаружили после взлома.

Почему так? Потому что:

Человек устаёт. Проверять безопасность — скучно. Нужно помнить 100500 правил, шаблонов, OWASP Top 10. Мозг отключается.

Нет стандартов. В одной команде знают про CSRF, в другой — нет. Всё зависит от опыта.

Это дорого. Нанять Security Engineer, который будет смотреть каждый MR? Не потянут маленькие компании.

Поэтому автоматизация — не опция. Это необходимость.

Как устроена автоматическая проверка в CI/CD

Ты добавляешь SAST-инструмент в pipeline, и он работает на каждый коммит. Вот пример с GitHub Actions и Semgrep:

name: Security Scan

on: [push, pull_request]

jobs:
  semgrep:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: returntocorp/semgrep-action@v1
        with:
          config: >-
            p/security-audit
            p/owasp-top-ten

Или с GitLab CI:

security_scan:
  stage: test
  script:
    - pip install bandit
    - bandit -r ./src/ -f json -o bandit-report.json
  artifacts:
    reports:
      sast: bandit-report.json

На каждый MR/PR — автоматическая проверка. Если нашлась уязвимость — блокируем merge до исправления.

Звучит просто? Но есть подводные камни.

Много false positives. Инструмент может пожаловаться на безопасный код. Приходится писать исключения, настраивать правила.

Нужно выбрать инструмент. SonarQube — мощный, но дорогой. Semgrep — проще, но менее гибкий. Bandit для Python, ESLint-плагины для JavaScript.

Разработчики начинают игнорировать предупреждения. Если каждый день видишь 50 warnings, перестаёшь обращать внимание.

Примеры реальных уязвимостей и как их ловить

Давайте я покажу несколько реальных случаев из своего опыта. И как их автоматически ловить.

Пример 1: SQL injection в параметре поиска

Код:

def search_users(query):
    sql = f"SELECT * FROM users WHERE name LIKE '%{query}%'"
    return db.execute(sql)

Пользователь вводит: %' OR '1'='1

Результат: получает все пользователей вместо результатов поиска.

Как ловить:

# Правильно — параметризованный запрос
def search_users(query):
    sql = "SELECT * FROM users WHERE name LIKE ?"
    return db.execute(sql, (f"%{query}%",))

SAST-инструмент (Bandit, Semgrep) найдёт f-string в SQL-запросе и выдаст warning. Semgrep правило:

rules:
  - id: sql-injection
    patterns:
      - pattern-either:
          - pattern: |
              $SQL = f"...{...}..."
              db.execute($SQL)
    message: "SQL injection risk"
    severity: ERROR

Пример 2: Hardcoded credentials

class DatabaseConfig:
    HOST = "db.example.com"
    USER = "admin"
    PASSWORD = "SuperSecret123!"

Это попадёт в git history. Даже если потом удалишь — останется в истории.

Как ловить:

Bandit находит это автоматически:

bandit -r ./
# Hardcoded SQL password: "SuperSecret123!"

Или используй git hooks:

# .git/hooks/pre-commit
git-secrets --scan

Лучше вообще хранить credentials в переменных окружения или vault:

import os
PASSWORD = os.getenv('DB_PASSWORD')

Пример 3: XSS в Django шаблоне

<!-- Опасно! -->
<h1>{{ user_comment }}</h1>

Если user_comment = "<script>alert('xss')</script>", скрипт выполнится.

Правильно:

<!-- Django автоматически экранирует -->
<h1>{{ user_comment|escape }}</h1>
<!-- Или просто использовать {{ user_comment }} — Django экранирует по умолчанию -->

Но нужно следить, чтобы не использовал |safe:

<!-- Опасно! -->
<h1>{{ user_comment|safe }}</h1>

SAST найдёт это правило. Semgrep:

rules:
  - id: django-xss
    pattern: "{{ ... |safe }}"
    message: "Potential XSS vulnerability"
    severity: WARNING

Пример 4: Path traversal в загрузке файлов

@app.post('/upload')
def upload_file():
    filename = request.files['file'].filename
    file.save(f'/uploads/{filename}')

Пользователь загружает файл с именем ../../etc/passwd — и перезаписывает важный файл.

Правильно:

import os
from werkzeug.utils import secure_filename

@app.post('/upload')
def upload_file():
    filename = secure_filename(request.files['file'].filename)
    # Проверяем, что filename не содержит ..
    if '..' in filename or filename.startswith('/'):
        return "Invalid filename", 400
    file.save(os.path.join('/uploads', filename))

secure_filename удалит все опасные символы. Но лучше быть явным и проверить.

Как внедрить автоматическую проверку безопасности

Шаг 1: Выбери инструмент. Для Python — Bandit или Semgrep. Для JavaScript — ESLint с плагинами безопасности. Для Java — SpotBugs. Для Go — gosec.

Шаг 2: Добавь в CI/CD. Выше я показал примеры для GitHub Actions и GitLab CI.

Шаг 3: Настрой правила. Отключи false positives, добавь свои правила для специфики твоего проекта.

Шаг 4: Блокируй мерж при критических уязвимостях.

# GitHub Actions
- name: Check SAST results
  if: failure()
  run: exit 1

Шаг 5: Обучи команду. Покажи разработчикам, как читать отчёты, как исправлять уязвимости.

Почему автоматизация лучше, чем ручная проверка

По моему опыту, ручная проверка ловит максимум 60% уязвимостей. Потому что:

Автоматизация:

Да, будут false positives. Но это лучше, чем пропустить реальную уязвимость.

На одном проекте мы внедрили Semgrep в pipeline. В первую неделю нашли 7 уязвимостей, которые раньше пропускали. Одна из них была SQL injection в админ-панели. Спасло проект от потенциального взлома.


Если ты ищешь решение, которое не требует настройки кучи инструментов — попробуй Distiq. Это AI-бот для code review, который встаёт прямо в твой GitLab или GitHub. Анализирует каждый MR/PR, ловит уязвимости, баги, проблемы с производительностью. Интегрируется за 2 минуты. Серверы в России, данные не уходят за рубеж.

Попробуйте Distiq для автоматического code review

AI-бот анализирует каждый MR/PR и оставляет комментарии с замечаниями. Интеграция за 2 минуты.

Попробовать бесплатно

Похожие статьи