Жизненный цикл безопасного ПО: языки, стеки, дефекты — лекция В.А. Пикова
Авторский курс · Secure SDLC и РБПО

Жизненный цикл безопасного ПО: языки, стеки и четыре дефекта

Один учебный день, одно приложение, полный путь: от требований и модели угроз до релиза с тестами и автоматическими проверками. Между ними — как устроена разработка сегодня, чем стек отличается от фреймворка, что даёт и чего не даёт выбор языка программирования, и как выглядит уязвимость, когда её видишь своими глазами.

5
пар, 10 академических часов
4
языка в сравнении
4
дефекта найти и исправить
19
тестов к концу дня
Тема программыТема № 4 — хроника языков, ЖЦ безопасного ПО
Нормативная опораГОСТ Р 56939-2024, процессы 5.8–5.18
Рабочий язык практикиPython, стандартная библиотека
ПреподавательПиков Виталий Александрович

Материалы занятия

Файлы открываются в браузере. Чтобы сохранить на диск — правая кнопка мыши, «Сохранить ссылку как…». Всё учебное приложение работает на стандартной библиотеке Python: ставить ничего не нужно.

01 Вопрос дня

Чем «программа работает» отличается от «программу можно выпускать»?

Это единственный вопрос, ради которого собран весь день. Ответ на него мы дадим в разделе 16 — а пока стоит записать свою версию на бумаге и не подглядывать.

Представьте: вы написали программу. Она запускается, считает правильно, вы проверили её на нескольких примерах. Всё сходится.

Готова ли она к тому, чтобы её выпустить?

Типичные ответы: «надо ещё потестировать», «надо написать документацию», «надо чтобы интерфейс был поприличнее». Всё это верно — и всё это описывает меньше половины того, что на самом деле отделяет работающий код от продукта, который можно отдать людям.

Забегая вперёд. В авторской модели этой лекции выпуск описывают 14 проверяемых свидетельств. Работающая программа — одно из них. Многие остальные можно полностью или частично собирать и проверять автоматически; решение о выпуске и принятии риска остаётся за ответственными людьми.

Что мы сделаем за день

1

Требования

Сформулируем, что приложение должно делать — и, главное, чего оно не должно делать никогда. Пять требований через «никогда» вечером превратятся в пять тестов.

Пара 1Лекция
2

Среда и первый код

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

Пара 2Практика
3

Архитектура

Перейдём от словарей к классам с встроенной проверкой. Посмотрим одну и ту же задачу на четырёх языках и увидим, что каждое поколение языков забирало у программиста.

Пара 3Практика
4

Дефекты

Запустим приложение с четырьмя намеренно оставленными уязвимостями. Увидим, как каждая срабатывает. Назовём класс по CWE. Исправим и проверим, что исправление работает.

Пара 4Ключевая
5

Проверки и релиз

Напишем тесты, которые не дадут уязвимости вернуться. Сравним линтер, SAST и SCA. Соберём конвейер и выпустим версию 1.0.

Пара 5Релиз

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

02 Пара 1 · Лекция

Жизненный цикл программного обеспечения

В этой лекции жизненный цикл представлен как семь этапов — это авторская учебная модель для разговора о свидетельствах выпуска.

требования → проектирование → реализация → тестирование → выпуск → сопровождение → вывод из эксплуатации

Стандарты и организации могут называть, объединять и повторять этапы по-разному. Методологии отличаются не только длиной итерации, но и ролями, артефактами, обратной связью и способом выпуска.

ПодходКак проходит циклГде применяется сегодня
ВодопадОдин раз, целиком, последовательноГосконтракты, сертифицируемые системы — там, где требования зафиксированы заранее и не меняются
ИтеративныйМного раз, каждый раз целиком, но по кусочку функционалаКрупные корпоративные системы
Agile / ScrumЦиклами по 1–4 недели, приоритеты пересматриваются каждый циклБольшая часть коммерческой разработки
DevOpsТо же плюс автоматизация выкладки: цикл замыкается на эксплуатациюСервисы, которые обновляются часто
DevSecOpsТо же, но проверки безопасности встроены в автоматику, а не пристроены сбокуТо, к чему идёт отрасль и на что ориентирован ГОСТ Р 56939
1970-е
Водопад
Один проход. Ошибка в требованиях доживает до сдачи
1990-е
Итерации, XP
Короткие циклы. Обратная связь появляется раньше
2000-е
Agile, SDL
Безопасность становится отдельным процессом
2010-е+
DevSecOps
Проверки безопасности внутри автоматики

Частое заблуждение. «Водопад устарел, все работают по Agile». В отрасли — да, во многом. Но в сертифицируемых системах и в госзаказе водопад никуда не делся и не денется: там требования фиксируются документом, который согласован с заказчиком и регулятором, и «пересмотреть приоритеты каждые две недели» просто не предусмотрено процедурой. Умение работать в обоих режимах — часть профессии.

03 Пара 1 · Лекция

Сдвиг влево и ГОСТ Р 56939-2024

Если нарисовать жизненный цикл слева направо, «сдвиг влево» — это перенос работ по безопасности из правой части в левую.

Из тестирования и выпуска — в требования, проектирование и написание кода. Причина простая и считается в деньгах: чем позже найден дефект, тем больше вокруг него успело нарасти.

1

Ошибка найдена в требованиях

Правится одним предложением в документе. Стоимость — минуты.

Дёшево
2

Та же ошибка в проектировании

Переделка схемы, пересогласование интерфейсов между компонентами.

Дни
3

Та же ошибка в коде

Переписывание модуля и всех тестов к нему. Плюс всё, что успело на него опереться.

Недели
4

Та же ошибка в выпущенном продукте

Плюс оповещение заказчиков, внеплановое обновление, разбор инцидента, объяснения регулятору, репутационные последствия.

Месяцы и деньги

Обратите внимание: сама работа по исправлению во всех четырёх случаях примерно одинаковая. Разница — в слое обвязки, который нарастает вокруг неё со временем. Сдвиг влево не про «делать больше работы», а про «делать ту же работу тогда, когда вокруг неё ещё ничего не наросло».

Что говорит стандарт

ГОСТ Р 56939-2024 «Защита информации. Разработка безопасного программного обеспечения. Общие требования» описывает 25 процессов с нумерацией 5.1–5.25 — от планирования и обучения сотрудников до реагирования на уязвимости и вывода ПО из эксплуатации.

SDL

Отвечает на вопрос как делать. Это инженерная практика: последовательность фаз от обучения до реагирования на инциденты.

  • Обучение
  • Требования
  • Проектирование
  • Реализация
  • Верификация
  • Выпуск
  • Реагирование

ГОСТ Р 56939-2024

Отвечает на вопрос что должно быть сделано и чем это подтверждается. Его требования могут входить в область оценки соответствия, когда стандарт применим; точный состав проверки задают схема оценки, договор и требования к продукту.

  • 25 процессов, 5.1–5.25
  • К каждому — регламент
  • К каждому — назначенный исполнитель
  • К каждому — запись о выполнении

Ключевое различие. Один и тот же статический анализ кода в SDL называется практикой, а в ГОСТ — процессом 5.10, к которому нужен регламент, исполнитель и запись о выполнении. Запустить анализатор — это практика. Показать проверяющему, кто, когда, по какому регламенту его запускал и что сделал с результатами — это процесс.

Процессы, которых касается сегодняшний день

ПроцессНазваниеГде сегодня
5.8Кодирование — правила и безопасные конструкцииПары 3 и 4 целиком
5.9Экспертиза исходного кодаРазбор дефектов, парная работа
5.10Статический анализ исходного кодаПара 5, ruff и bandit
5.12Использование безопасной системы сборкиПара 3, флаги компилятора в Object Pascal
5.16Композиционный анализПара 5, pip-audit и SBOM
5.18Функциональное тестированиеПара 5, регрессионные тесты
04 Пара 1 · Лекция

Экстремальное программирование

Вторая половина 1990-х. Кент Бек, книга «Extreme Programming Explained» — 1999 год.

Идея заложена в названии. Возьмём практики, которые все считают полезными, и доведём их до предела:

  • Ревью кода полезно? Тогда пусть код пишут двое за одним экраном — ревью станет непрерывным.
  • Тесты полезны? Тогда будем писать тест до кода.
  • Интеграция полезна? Тогда будем интегрировать несколько раз в день, а не раз в квартал.

Двенадцать практик

Планирование
Малые релизы
Метафора системы
Простой дизайн
Тестирование
Рефакторинг
Парное программирование
Коллективный код
Непрерывная интеграция
40-часовая неделя
Заказчик рядом
Стандарт кодирования

Четыре практики XP, которые являются мерами безопасности

Практика XPЧто это в терминах РБПО
Парное программированиеЭкспертиза исходного кода (процесс 5.9), выполняемая непрерывно, а не отдельной процедурой раз в спринт
Тестирование до кода (TDD)Каждый исправленный дефект получает тест. Регрессия закрыта навсегда, а не до следующего рефакторинга
Непрерывная интеграцияОснова конвейера РБПО: проверки запускаются автоматически, а не когда о них вспомнили
Стандарт кодированияПравила кодирования — прямое требование процесса 5.8

Честная оговорка. XP в чистом виде сегодня применяют редко. Но непрерывная интеграция, разработка через тестирование и парное программирование ушли из него в общую практику и живут там отдельно от методологии. Это тот случай, когда подход растворился в отрасли — и это лучшее, что может случиться с методологией.

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

05 Пара 2 · Стек

Десять слоёв современной разработки

Когда говорят «стек технологий», имеют в виду набор выборов — по одному на каждом слое. Слоёв примерно десять.

1

Язык и среда выполнения

Python, TypeScript, Java, C#, Go, Rust, Kotlin, Swift

2

Пакеты и изолированное окружение

pip / uv, npm / pnpm, NuGet, Maven, Cargo, Go modules

3

Фреймворк

Django, FastAPI, React, Vue, Spring Boot, ASP.NET Core

4

Хранилище данных

PostgreSQL, SQLite, Redis, ClickHouse, MongoDB

5

Упаковка и перенос

Docker, Docker Compose, Kubernetes

6

Контроль версий

Git + GitLab, GitVerse, GitFlic, Gitea. Предмет процесса 5.4

7

CI/CD

GitLab CI, GitHub Actions, Jenkins, TeamCity. Здесь живёт конвейер РБПО

8

Качество и безопасность

Линтер, тесты, SAST, SCA, DAST, фаззинг. Процессы 5.10, 5.11, 5.16, 5.18

9

Наблюдаемость

Логи, метрики, трассировки. Как понять, что происходит в работе

10

Помощь разработчику

IDE, отладчик, ИИ-ассистенты

Граница модели. Схема из 10 слоёв — авторская дидактическая модель, а не структура стандарта и не статистика учебных программ. Она помогает увидеть, что выпуск требует не только кода, но и управления версиями, конвейера, проверок и эксплуатации. Точный объём проверки при оценке соответствия определяют применимая схема, требования заказчика и модель угроз продукта; его нельзя свести к фиксированным слоям 6–8.

06 Пара 2 · Стек

Где какой язык живёт

Вопрос «какой язык лучше» не имеет ответа. Вопрос «какой язык для какой задачи» — имеет.

ЯзыкОсновные нишиТипичные фреймворки
PythonВеб-бэкенд, анализ данных и ML, автоматизация, скрипты ИБDjango, FastAPI, Flask
JavaScript / TypeScriptВеб-интерфейсы, серверная частьReact, Vue, Svelte, Node.js
JavaКрупные корпоративные системы, AndroidSpring Boot
C#Корпоративные и настольные приложения, игрыASP.NET Core, .NET MAUI, Unity
C / C++Операционные системы, драйверы, встраиваемые системыQt, Boost
RustТо же, что C/C++, но с гарантиями безопасности памятиTokio, Axum
GoСетевые сервисы, инфраструктурное ПОстандартная библиотека, Gin
Kotlin / SwiftМобильные приложенияJetpack Compose, SwiftUI
SQLРабота с данными. Декларативный язык — тема среды
Учётные системы, в России очень широкоплатформа 1С:Предприятие
Object Pascal (Delphi)Сопровождение унаследованных систем, быстрая разработка интерфейсовVCL, Lazarus
АссемблерОбратная разработка, загрузчики, оптимизация узких мест

Расписание сегодняшнего дня называет четыре языка: Assembler, Python, Delphi, C#. Выбор не случайный — это четыре разных уровня отношений с памятью, и в разделе 10 мы посмотрим на них на одной и той же задаче.

07 Пара 2 · Стек

Библиотека, фреймворк, платформа

Различие определяется одним признаком — кто кого вызывает.

Библиотеку вызываете вы

Вы решаете, когда обратиться к requests, чтобы отправить запрос. Управление остаётся у вас.

requestssqlite3pandas

Фреймворк вызывает вас

Django сам решает, когда вызвать вашу функцию обработки страницы. Это называется инверсией управления.

DjangoReactASP.NET Core

Платформа — .NET, JVM, Docker — определяет, где и как вы вообще можете быть вызваны. Она задаёт правила игры до того, как в неё вступит и ваш код, и фреймворк.

Почему это важно для безопасности. Когда чужой код управляет вашим, уязвимость в нём становится вашей уязвимостью — и вы о ней не узнаете, пока специально не начнёте следить. Отсюда композиционный анализ (процесс 5.16) и SBOM: перечень всех компонентов, входящих в продукт. Без него на вопрос «а у вас есть эта уязвимая библиотека?» приходится отвечать «сейчас поищем», и поиск занимает недели.

Кто ещё участвует в разработке

Часть кода сегодня создаётся с помощью ИИ-ассистентов, и это уже обычная практика, а не экзотика. Для РБПО важны три вещи.

01

Ответственность не делится

  • Отвечает тот, кто отправил код в репозиторий
  • Сгенерированный код проходит те же процессы 5.8–5.11
  • «Так сгенерировал ассистент» — не объяснение для проверяющего
02

Объём растёт быстрее внимания

  • Кода становится больше, а глаз — столько же
  • Это усиливает роль автоматических проверок
  • Ручное ревью перестаёт масштабироваться первым
03

Новый риск цепочки поставок

  • Ассистент может предложить несуществующий пакет
  • Злоумышленник может заранее занять это имя
  • Проверка имён зависимостей стала обязательной

Практическое правило. Пользоваться ассистентами можно, и в этом нет ничего плохого. Но не отправляйте в коммит строку, которую не можете объяснить. Как только вы нажали «закоммитить» — это ваш код и ваша ответственность.

08 Пара 3 · Архитектура

Задача дня: журнал учёта носителей

Приложение намеренно маленькое. Всё интересное сегодня — не в нём, а вокруг него.

Учётная запись: инвентарный номер, тип носителя, гриф, ответственный.
Функции: добавить носитель, найти по номеру или ФИО, выгрузить отчёт в файл.

Требования безопасности через «никогда»

Формулировка через «никогда» — не стилистический приём. Такое требование почти механически превращается в тест, и мы это увидим в разделе 13.

  1. Поиск для обычного пользователя никогда не возвращает записи с грифом «Конфиденциально».
  2. Отчёт никогда не записывается за пределы каталога выгрузки.
  3. Служебный токен никогда не хранится в исходном коде.
  4. Служебный токен никогда не попадает в журнал приложения.
  5. Запись с невалидными данными никогда не попадает в журнал.

Мини-модель угроз

АктивУгрозаКтоМера
Перечень конфиденциальных носителейРаскрытие через функцию поискаВнутренний пользователь без правФильтр по грифу + регрессионный тест
Файлы за пределами каталогаПерезапись произвольного файлаВнутренний пользовательНормализация пути и проверка границы
Служебный токенКомпрометация через репозиторийЛюбой с доступом к gitСекрет вне кода, .gitignore
Служебный токенКомпрометация через журналыОператор, служба поддержкиМаскирование при записи в журнал
Целостность журнала учётаВнесение мусорных записейПользователь, ошибка интеграцииВалидация в конструкторе объекта

Запомните это место. Пять строк «никогда» и пять строк модели угроз, написанные за 15 минут в начале дня, к вечеру превратятся в пять тестов, которые будут проверяться автоматически при каждом изменении кода. Это и есть сдвиг влево в самом дешёвом исполнении.

09 Пара 3 · Архитектура

От словаря к классу

Первая версия журнала хранит записи в словарях. Она работает. И в ней уже три дефекта.

Версия 1 — работает, но беззащитна
record = {"inv": "МНИ-001", "kind": "USB-флеш",
          "label": "ДСП", "owner": "Иванова А. П."}
ПроблемаЧто произойдёт
У словаря нет схемыОпечатка record["Owner"] вместо record["owner"] не вызовет ошибки — просто создастся новый ключ, и данные молча разойдутся
Нет проверки значенийТип «Дискета», гриф «Совершенно секретно», ФИО «asdf» — всё пройдёт без единого возражения
Нет защиты от дублейОдин инвентарный номер можно добавить дважды
Версия 2 — невалидное состояние невозможно
@dataclass(frozen=True)
class Medium:
    inv: str
    kind: str
    label: str
    owner: str

    def __post_init__(self) -> None:
        if not INV_PATTERN.match(self.inv):
            raise ValidationError(...)
        if self.kind not in ALLOWED_KINDS:
            raise ValidationError(...)

Три изменения, и каждое — мера безопасности:

  • Набор полей фиксирован. Опечатка в имени поля теперь ошибка, а не тихое расхождение данных.
  • frozen=True — запись неизменяема. Учётную запись нельзя тихо поменять в обход журнала. Значит, все изменения проходят через журнал. Значит, все изменения можно записать.
  • Проверка в конструкторе. Невалидный объект нельзя создать вообще — ни из формы, ни из импорта CSV, ни из теста, ни из кода, который ещё не написан.

Ключевая мысль раздела. Проверять данные при вводе — это охранять одну дверь. Проверять в конструкторе — сделать так, чтобы неправильное состояние было невозможно в принципе. Первое — контроль. Второе — свойство системы. Контроль можно обойти, свойство — нет.

Белый список против чёрного

В справочниках мы перечисляем разрешённые типы носителей и грифы, а не запрещённые.

ALLOWED_KINDS = ("USB-флеш", "HDD", "SSD", "CD/DVD", "Карта памяти")
ALLOWED_LABELS = ("Открыто", "ДСП", "Конфиденциально")

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

10 Пара 3 · Архитектура

Четыре языка, одна задача

Задача: скопировать строку в буфер размером ровно 16 символов. Строка длиннее буфера. Четыре языка — четыре разных ответа на вопрос «а кто за этим следит».

1 / 4Ассемблер — не следит никто
copy_name:
    lea     rdi, [rel buffer]   ; куда копируем
.loop:
    mov     al, [rsi]           ; взять байт источника
    mov     [rdi], al           ; положить в приёмник
    inc     rsi
    inc     rdi
    test    al, al              ; дошли до нуля?
    jnz     .loop               ; нет — продолжаем
    ret

Цикл копирует до нулевого байта — сколько бы их ни было. Проверки размера нет нигде. Строка в 40 байт положит 24 лишних байта в соседнюю память.

Процессор не считает это ошибкой. Для него нет никакого «буфера» — есть адрес и инструкция записи. Понятие границы существует только в голове программиста и в исходном тексте.

Это CWE-787, запись за границей буфера — корневая причина классического переполнения буфера.

2 / 4Object Pascal — следит компилятор, если его попросить
{$RANGECHECKS ON}
type TBuffer = array[0..15] of Char;

При включённой директиве программа аварийно остановится на 17-м символе: Runtime error 201. При выключенной — молча продолжит писать за границу, ровно как ассемблер.

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

Именно поэтому в РБПО проверяется не только исходный код, но и параметры компиляции — это процесс 5.12 «Использование безопасной системы сборки». Флаги компилятора являются таким же объектом контроля, как и текст программы.

3 / 4C# — управляемый код проверяет среда выполнения
char[] buffer = new char[16];
buffer[i] = source[i];   // IndexOutOfRangeException при i >= 16

Проверку границ нельзя отключить настройкой сборки — она встроена в среду выполнения CLR.

В самом управляемом массиве эта проверка сохраняется. Отдельную границу создают явный unsafe-код, FFI/P/Invoke и нативные зависимости: они требуют разрешения сборки, ревью и собственных мер контроля.

Поэтому правила кодирования для C#-проектов обычно запрещают или отдельно согласуют unsafe и межъязыковые границы, а не считают сам выбор языка полной защитой.

4 / 4Python — вопрос исчезает вместе с ручной памятью
buffer = [""] * 16
buffer[i] = source[i]   # IndexError

# а так эту задачу пишут в реальной жизни:
result = source[:16]   # срез никогда не выходит за границы

Проверка всегда, отключить нельзя. Более того, у строк в Python вообще нет фиксированного размера — задача в таком виде обычно просто не возникает.

Плата за это — производительность и потеря контроля над тем, где и как размещаются данные. Поэтому загрузчики, драйверы и ядра операционных систем на Python не пишут.

Итоговая таблица

ЯзыкКто отвечает за границы памятиМожно ли отключить проверку
АссемблерПрограммистПроверки нет вообще
Object PascalПрограммист + компиляторДа, директивой
C#Среда выполнения (CLR) в управляемом кодеunsafe и нативные границы — отдельный контур
PythonИнтерпретатор в обычном Python-кодеНативные расширения — отдельный контур

Языки, где безопасность памяти обеспечивается средой или компилятором, называют memory-safe: C#, Java, Python, Go, Rust. Языки, где она лежит на программисте: C, C++, ассемблер.

Исторический ориентир: около 70 %. В публикации MSRC указано, что около 70 % уязвимостей, которым Microsoft назначала CVE, относились к проблемам безопасности памяти. Это контекст продуктов Microsoft, а не универсальная доля для всех систем. Управляемые языки и безопасное подмножество Rust устраняют многие такие ошибки в обычном коде, но unsafe, FFI и нативные расширения требуют отдельного контроля.

Главный вывод пары. Выбор языка — это архитектурное решение по безопасности, принятое до написания первой строки кода. Оно определяет, какие классы уязвимостей вообще возможны в вашей программе.

Но ни один язык не защищает от SQL-инъекции, от ошибки в логике доступа и от секрета, попавшего в репозиторий. Именно их мы сейчас и пойдём ловить.

11 Пара 4 · Дефекты

Четыре дефекта безопасности

Все четыре живут в файле step3_defect.py. Их нужно найти, назвать по CWE и исправить. Порядок разбора — от самого наглядного.

Про учебный файл. step3_defect.py содержит намеренно оставленные уязвимости и предназначен только для разбора на занятии. В рабочие проекты этот код не переносится. Исправленный вариант — step3_fixed.py.

Д3 · Внедрение SQL-кода · CWE-89

Функция find_public по замыслу обязана возвращать только записи с грифом «Открыто».

Как написано
sql = ("SELECT inv, kind, label, owner FROM media "
       "WHERE label = 'Открыто' AND owner LIKE '%" + query + "%'")

Введём строку %' OR label LIKE '% — и получится вот такой запрос:

SELECT ... WHERE label = 'Открыто' AND owner LIKE '%%' OR label LIKE '%%'

В SQL операция AND связывает сильнее, чем OR. Значит, условие читается так:

(label = 'Открыто' AND owner LIKE '%%')   ИЛИ   (label LIKE '%%')

Второе слагаемое истинно для любой строки: % в LIKE означает «любая последовательность символов». Дизъюнкция с истиной даёт истину — и фильтр по грифу перестаёт что-либо значить.

Контрольный вывод программы
  Тот же поиск, но пользователь ввёл специальную строку:
      ('МНИ-001', 'USB-флеш', 'Открыто', 'Иванова А. П.')
      ('МНИ-002', 'HDD', 'Открыто', 'Петров С. И.')
      ('МНИ-003', 'SSD', 'Конфиденциально', 'Сидорова М. К.')
      ('МНИ-004', 'USB-флеш', 'Конфиденциально', 'Кузнецов Д. А.')

  Утекло записей с грифом «Конфиденциально»: 2
  Функция обязана была вернуть 0 таких записей.
Как надо — параметризованный запрос
sql = ("SELECT inv, kind, label, owner FROM media "
       "WHERE label = 'Открыто' AND owner LIKE ?")
return connection.execute(sql, (f"%{query}%",)).fetchall()

Текст запроса — константа, известная на этапе написания кода. Значения передаются отдельно. База разбирает запрос до того, как увидит данные — поэтому данные уже не могут стать командой.

Почему нельзя «просто запретить кавычки». Это фильтрация по чёрному списку: способов записать то же самое всегда больше, чем вы предусмотрели, а фамилии с апострофом при этом перестанут искаться.

И главное: инъекция — это не про SQL. Это про склейку команды из данных. То же самое бывает с командами оболочки, путями к файлам, шаблонами, LDAP-запросами, заголовками HTTP. Как только вы строите строку-команду конкатенацией — вы в зоне риска.

Д4 · Выход за пределы каталога · CWE-22

Как написано
path = Path(os.path.join(EXPORT_DIR, filename))

Имя файла приходит снаружи. Передайте ../../отчёт.txt — и файл уйдёт за пределы каталога выгрузки, туда, куда программе писать не положено.

Как надо — нормализация и проверка границы
base = EXPORT_DIR.resolve()
candidate = (base / filename.replace("\\", "/")).resolve()
if not candidate.is_relative_to(base):
    raise ValueError("Имя файла выводит за пределы каталога выгрузки")

resolve() приводит путь к каноническому виду и убирает ... Затем проверяем, что результат действительно лежит внутри разрешённого каталога.

Правило. Проверять исходную строку на наличие .. ненадёжно: есть обратные слэши, процентное кодирование, символические ссылки, подкаталоги вида a/../../b. Проверяйте результат, а не текст.

Д1 · Секрет в исходном коде · CWE-798

Как написано
ADMIN_TOKEN = "s3cr3t-admin-token-training"

Такой токен уезжает в систему контроля версий и остаётся в её истории навсегда. Удаление строки следующим коммитом ничего не меняет: старые коммиты никуда не делись, и git log -p их покажет. Сменить токен можно только пересборкой и перевыпуском программы.

Как надо
def get_admin_token() -> str:
    token = os.environ.get("MEDIA_JOURNAL_ADMIN_TOKEN")
    if not token:
        raise RuntimeError("Не задана переменная окружения...")
    return token

В коде остаётся только имя переменной. Сам секрет живёт в окружении процесса, в файле .env (добавленном в .gitignore) или в менеджере секретов.

Обратите внимание на деталь. При отсутствующем секрете программа падает, а не подставляет значение по умолчанию. Значение по умолчанию для секрета — это тот же захардкоженный секрет, только незаметный.

Д2 · Секрет в журнале приложения · CWE-532

Как написано
logging.info("Попытка входа: пользователь=%s токен=%s", user, token)

Журналы читает больше людей, чем исходный код: их собирают в SIEM, выгружают в тикеты, прикладывают к обращениям в поддержку. Секрет в журнале — это секрет, розданный всем этим людям.

Как надо — маскирование
def mask(secret: str) -> str:
    if len(secret) <= 4:
        return "***"
    return f"{secret[:2]}***{secret[-2:]} (длина {len(secret)})"

logging.info("Попытка входа: пользователь=%s токен=%s", user, mask(token))

Заодно в эталоне сравнение токенов сделано через hmac.compare_digest: обычное == завершается на первом несовпавшем символе, поэтому по времени ответа секрет можно подобрать посимвольно.

Живая деталь из подготовки материалов. hmac.compare_digest со строками принимает только ASCII и падает на кириллице — поэтому в эталоне стоит .encode("utf-8") и сравниваются байты. Хорошая иллюстрация того, что «правильная» функция тоже требует чтения документации.

12 Пара 4 · Дефекты

Чего не лечит выбор языка

Все четыре дефекта из предыдущего раздела воспроизводятся на любом языке — на C#, на Java, на Go. Memory safety тут не помогает вообще.

ДефектCWEКатегория OWASP Top 10 (ред. 2021)
Д3 · Внедрение SQL-кодаCWE-89A03 · Injection
Д4 · Выход за пределы каталогаCWE-22A01 · Broken Access Control
Д1 · Секрет в исходном кодеCWE-798A07 · Identification and Authentication Failures
Д2 · Секрет в журналеCWE-532A09 · Security Logging and Monitoring Failures
Переполнение буфера (раздел 10)CWE-787вне веб-контекста OWASP

Три класса дефектов, от которых язык не спасает

01

Внедрение кода

  • SQL, команды оболочки, пути, шаблоны, LDAP
  • Ошибка построения команды из данных
  • Возможна на любом языке без исключений
02

Дефекты логики доступа

  • Программа корректно выполняет неправильное правило
  • Компилятор здесь бессилен по определению
  • Ловится только тестами на требования
03

Секреты и цепочка поставок

  • Организационно-процессная проблема
  • К языку отношения не имеет вообще
  • Лечится дисциплиной и автоматикой

Что из этого следует для РБПО. Именно поэтому ГОСТ Р 56939-2024 описывает 25 процессов, а не один пункт «пишите на безопасном языке». Безопасный язык закрывает один класс дефектов — важный, но один. Остальные закрываются требованиями, архитектурой, правилами кодирования, экспертизой кода, тестами, статическим и динамическим анализом, управлением конфигурацией и работой с уязвимостями.

13 Пара 5 · Проверки

Тесты и регрессии безопасности

Два вида тестов, и разница между ними — главная мысль последней пары.

Функциональные

Проверяют, что программа делает то, что должна. Добавляет запись, находит по фамилии, считает количество.

Регрессии безопасности

Проверяют, что программа не делает того, чего не должна. Это память об уже найденной уязвимости.

Пока регрессионный тест зелёный, дефект не может вернуться в код незамеченным. Это процесс 5.18 ГОСТ Р 56939-2024 в самом дешёвом исполнении: одна функция вместо целого регламента.

Требование «никогда» № 1 из раздела 08, дословно превращённое в тест
def test_d3_sql_injection_does_not_leak_confidential(self):
    payload = "%' OR label LIKE '%"
    rows = step3_fixed.find_public(self.connection, payload)
    leaked = [row for row in rows if row[2] == "Конфиденциально"]
    self.assertEqual(leaked, [])

Замкните круг вслух. Утром на доске было написано: «поиск никогда не возвращает записи с грифом Конфиденциально». Вечером это ровно тот же текст, только исполняемый. Требование безопасности, сформулированное через «никогда», превращается в тест почти механически — поэтому и формулировать их стоит именно так.

Красное перед зелёным

Тест, который никогда не падал, ничего не доказывает. Прежде чем поверить тесту, убедитесь, что он умеет краснеть.

# подставляем дефектную версию вместо исправленной
import step3_defect as step3_fixed
Контрольный результат эталона
# на исправленной версии
Ran 19 tests in 0.006s
OK

# на дефектной версии
Ran 8 tests in 0.012s
FAILED (failures=6, errors=9)

Тесты падают — значит, они действительно проверяют то, что заявлено. Только теперь им можно верить.

14 Пара 5 · Проверки

Линтер, SAST, SCA — разные инструменты

Их часто валят в одну кучу под словом «анализаторы». Это разные инструменты для разных задач, и подмена одного другим — типичная ошибка при внедрении.

Пример вывода ruff на дефектном файле
$ py -m ruff check step3_defect.py

F841 Local variable `lines` is assigned to but never used
   --> step3_defect.py:105:5
Found 1 error.

Остановитесь здесь на минуту. Линтер нашёл неиспользуемую переменную — и не нашёл SQL-инъекцию, которая живёт в том же файле и через которую утекают конфиденциальные записи.

Это не недостаток ruff. Это разница в назначении. Линтер отвечает за стиль, мёртвый код и очевидные ошибки. Поиск опасных конструкций — задача анализатора безопасности.

ИнструментЧто ищетНашёл ли Д3
ТестыНарушение заданного поведенияДа, если тест написан
ruff · линтерСтиль, мёртвый код, опечаткиНет
bandit · SASTОпасные конструкции языкаКандидат: B608
pip-audit · SCAУязвимости в чужих зависимостяхНет — другой класс

В текущем учебном файле ожидаются правила B105 hardcoded_password_string для константы ADMIN_TOKEN и B608 hardcoded_sql_expressions для вероятного формирования SQL-строки. B608 — эвристический кандидат, а не доказательство эксплуатируемости или отсутствия SQL-инъекции: возможны ложные срабатывания и пропуски.

Вывод раздела. Ни один инструмент не закрывает задачу в одиночку. Отсюда конвейер, в котором они стоят последовательно — и отсюда же то, что в ГОСТ Р 56939-2024 статический анализ (5.10), динамический анализ (5.11), композиционный анализ (5.16) и функциональное тестирование (5.18) записаны как четыре разных процесса, а не один.

15 Пара 5 · Проверки

Конвейер и релиз

Конвейер РБПО — это те же самые команды, которые вы вводили руками, только запускаемые автоматически при каждом изменении кода.

stages: [lint, test, security]

lint:
  script:
    - ruff check .

test:
  script:
    - python -m unittest -v test_journal.py

sast:
  script:
    - bandit -r .

sca:
  script:
    - pip-audit --requirement requirements-dev.txt
  allow_failure: true

В этом и состоит вся идея непрерывной интеграции: проверки выполняются не тогда, когда о них вспомнили, а всегда.

Quality gate

Шаг, отрицательный результат которого останавливает сборку, называется quality gate.

ШагБлокирует?Почему
testВсегдаКрасный тест означает, что программа делает не то, что заявлено. Выпускать такое нельзя
sastПо высокой критичностиНаходка высокого уровня — это потенциальная уязвимость в продукте
lintСначала нет, потом даНа старте внедрения предупреждает, иначе замечания копятся и обесцениваются
scaЧасто предупреждаетУязвимость в чужой библиотеке не всегда можно закрыть сегодня

Конвейер без исполняемой политики превращается в генератор отчётов. Если у результата нет владельца, порога, срока реакции и процедуры принятия исключения, он рискует остаться пассивной телеметрией. Это наблюдение о риске процесса и не является универсальной статистикой срока.

Мини-паспорт РБПО учебного приложения

ПолеЗначение
Наименование и версияЖурнал учёта МНИ, v1.0
Язык и средаPython 3.x, стандартная библиотека
Правила кодированияСтандарт PEP 8, контроль ruff
Контроль вводаВалидация в конструкторе, справочники по белому списку
Работа с секретамиПеременные окружения, .gitignore, маскирование в журналах
Тестированиеunittest, 19 тестов, из них 9 регрессионных по безопасности
Статический анализruff, bandit
Управление конфигурациейgit, тег v1.0
Известные ограниченияНет аутентификации пользователей, нет разграничения доступа, нет шифрования хранилища

Последняя строка обязательна. Честный перечень того, чего в продукте нет, — признак зрелого процесса, а не слабости. Продукт без списка ограничений означает одно из двух: либо его не анализировали, либо список есть, но его прячут. Оба варианта хуже, чем честная строка.

16 Итог

Ответ на вопрос дня

Чем «программа работает» отличается от «программу можно выпускать»?

Разницей в проверяемых свидетельствах. Работающий код — это одно свидетельство. Вот полный список:

  • Сформулированные требования, в том числе требования безопасности через «никогда»
  • Модель угроз — пусть на пять строк, но написанная
  • Контроль ввода, встроенный в структуру данных, а не приделанный сбоку
  • Работающий код — то самое единственное свидетельство, с которого все начинают
  • Функциональные тесты
  • Регрессионные тесты безопасности, проверенные на дефектной версии
  • Отчёт линтера
  • Отчёт анализатора безопасности
  • Отчёт композиционного анализа зависимостей
  • История изменений в системе контроля версий
  • Отсутствие секретов в коде и в истории репозитория
  • Воспроизводимая сборка с зафиксированными параметрами компиляции
  • Зафиксированная версия выпуска
  • Честный перечень того, чего в продукте нет

И главное. Многие свидетельства можно полностью или частично собирать и проверять автоматически. Требования, модель угроз, решения об исключениях и финальное принятие риска требуют ответственных людей. Сдвиг влево сокращает обратную связь, но не отменяет контроль перед выпуском и в эксплуатации.

Связанные учебные модули

МодульТемаСвязь с этой лекцией
АвтоматизацияТестирование ПО, CI/CD, GitLab CEПродолжение разделов 13 и 15
Языки и архитектураХроника языков, декларативные языки (Prolog, SQL), сдвиг влево, фреймворки, архитектураПродолжение разделов 5, 6 и 10. Здесь SQL встретился как источник уязвимости; в связанном модуле он разбирается как язык
Конвейер РБПОСтатический, композиционный и динамический анализ, фаззингПродолжение раздела 14 вглубь
Итоговый модульСведение свидетельств и итоговая проверкаПрименение полного набора критериев выпуска

Скачать все материалы одним архивом

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

Скачать материалы занятия (ZIP) Один компактный архив · распаковать в любую папку
«From Working Code to Shippable Product» (HTML) 14 смысловых экранов · доступный текст, источники и оговорки · входит в ZIP-архив

Что внутри и с чего начать. Распакуйте архив и откройте ЧИТАТЬ-ПЕРВЫМ.md — там порядок работы. Лекция — index.html, конспект и задания — в каталоге materials/, весь код — в code/. Учебное приложение работает на стандартной библиотеке Python: ставить дополнительно ничего не нужно.

Что почитать

  • ГОСТ Р 56939-2024 — выборочно по процессам сегодняшнего дня: 5.8, 5.9, 5.10, 5.12, 5.18
  • Банк данных угроз ФСТЭК Россииbdu.fstec.ru
  • CWEcwe.mitre.org, поиск по номерам занятия: CWE-89, CWE-22, CWE-798, CWE-532, CWE-787
  • OWASP Top 10 — перечень наиболее критичных рисков веб-приложений
  • Kent Beck. Extreme Programming Explained — первоисточник по XP