Жизненный цикл безопасного ПО: языки, стеки и четыре дефекта
Один учебный день, одно приложение, полный путь: от требований и модели угроз до релиза с тестами и автоматическими проверками. Между ними — как устроена разработка сегодня, чем стек отличается от фреймворка, что даёт и чего не даёт выбор языка программирования, и как выглядит уязвимость, когда её видишь своими глазами.
Материалы занятия
Файлы открываются в браузере. Чтобы сохранить на диск — правая кнопка мыши, «Сохранить ссылку как…». Всё учебное приложение работает на стандартной библиотеке Python: ставить ничего не нужно.
Чем «программа работает» отличается от «программу можно выпускать»?
Это единственный вопрос, ради которого собран весь день. Ответ на него мы дадим в разделе 16 — а пока стоит записать свою версию на бумаге и не подглядывать.
Представьте: вы написали программу. Она запускается, считает правильно, вы проверили её на нескольких примерах. Всё сходится.
Готова ли она к тому, чтобы её выпустить?
Типичные ответы: «надо ещё потестировать», «надо написать документацию», «надо чтобы интерфейс был поприличнее». Всё это верно — и всё это описывает меньше половины того, что на самом деле отделяет работающий код от продукта, который можно отдать людям.
Забегая вперёд. В авторской модели этой лекции выпуск описывают 14 проверяемых свидетельств. Работающая программа — одно из них. Многие остальные можно полностью или частично собирать и проверять автоматически; решение о выпуске и принятии риска остаётся за ответственными людьми.
Что мы сделаем за день
Требования
Сформулируем, что приложение должно делать — и, главное, чего оно не должно делать никогда. Пять требований через «никогда» вечером превратятся в пять тестов.
Среда и первый код
Разберём, как устроена разработка сегодня. Соберём рабочее место, возьмём код под контроль версий, получим первую работающую версию.
Архитектура
Перейдём от словарей к классам с встроенной проверкой. Посмотрим одну и ту же задачу на четырёх языках и увидим, что каждое поколение языков забирало у программиста.
Дефекты
Запустим приложение с четырьмя намеренно оставленными уязвимостями. Увидим, как каждая срабатывает. Назовём класс по CWE. Исправим и проверим, что исправление работает.
Проверки и релиз
Напишем тесты, которые не дадут уязвимости вернуться. Сравним линтер, SAST и SCA. Соберём конвейер и выпустим версию 1.0.
Приложение одно и то же на протяжении всего дня — журнал учёта машинных носителей информации. Предметная область знакомая: инвентарный номер, тип носителя, гриф, ответственный. Задачи простые: добавить, найти, выгрузить отчёт. Именно поэтому на нём хорошо видно всё остальное.
Жизненный цикл программного обеспечения
В этой лекции жизненный цикл представлен как семь этапов — это авторская учебная модель для разговора о свидетельствах выпуска.
требования → проектирование → реализация → тестирование → выпуск → сопровождение → вывод из эксплуатации
Стандарты и организации могут называть, объединять и повторять этапы по-разному. Методологии отличаются не только длиной итерации, но и ролями, артефактами, обратной связью и способом выпуска.
| Подход | Как проходит цикл | Где применяется сегодня |
|---|---|---|
| Водопад | Один раз, целиком, последовательно | Госконтракты, сертифицируемые системы — там, где требования зафиксированы заранее и не меняются |
| Итеративный | Много раз, каждый раз целиком, но по кусочку функционала | Крупные корпоративные системы |
| Agile / Scrum | Циклами по 1–4 недели, приоритеты пересматриваются каждый цикл | Большая часть коммерческой разработки |
| DevOps | То же плюс автоматизация выкладки: цикл замыкается на эксплуатацию | Сервисы, которые обновляются часто |
| DevSecOps | То же, но проверки безопасности встроены в автоматику, а не пристроены сбоку | То, к чему идёт отрасль и на что ориентирован ГОСТ Р 56939 |
Частое заблуждение. «Водопад устарел, все работают по Agile». В отрасли — да, во многом. Но в сертифицируемых системах и в госзаказе водопад никуда не делся и не денется: там требования фиксируются документом, который согласован с заказчиком и регулятором, и «пересмотреть приоритеты каждые две недели» просто не предусмотрено процедурой. Умение работать в обоих режимах — часть профессии.
Сдвиг влево и ГОСТ Р 56939-2024
Если нарисовать жизненный цикл слева направо, «сдвиг влево» — это перенос работ по безопасности из правой части в левую.
Из тестирования и выпуска — в требования, проектирование и написание кода. Причина простая и считается в деньгах: чем позже найден дефект, тем больше вокруг него успело нарасти.
Ошибка найдена в требованиях
Правится одним предложением в документе. Стоимость — минуты.
Та же ошибка в проектировании
Переделка схемы, пересогласование интерфейсов между компонентами.
Та же ошибка в коде
Переписывание модуля и всех тестов к нему. Плюс всё, что успело на него опереться.
Та же ошибка в выпущенном продукте
Плюс оповещение заказчиков, внеплановое обновление, разбор инцидента, объяснения регулятору, репутационные последствия.
Обратите внимание: сама работа по исправлению во всех четырёх случаях примерно одинаковая. Разница — в слое обвязки, который нарастает вокруг неё со временем. Сдвиг влево не про «делать больше работы», а про «делать ту же работу тогда, когда вокруг неё ещё ничего не наросло».
Что говорит стандарт
ГОСТ Р 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, регрессионные тесты |
Экстремальное программирование
Вторая половина 1990-х. Кент Бек, книга «Extreme Programming Explained» — 1999 год.
Идея заложена в названии. Возьмём практики, которые все считают полезными, и доведём их до предела:
- Ревью кода полезно? Тогда пусть код пишут двое за одним экраном — ревью станет непрерывным.
- Тесты полезны? Тогда будем писать тест до кода.
- Интеграция полезна? Тогда будем интегрировать несколько раз в день, а не раз в квартал.
Двенадцать практик
Четыре практики XP, которые являются мерами безопасности
| Практика XP | Что это в терминах РБПО |
|---|---|
| Парное программирование | Экспертиза исходного кода (процесс 5.9), выполняемая непрерывно, а не отдельной процедурой раз в спринт |
| Тестирование до кода (TDD) | Каждый исправленный дефект получает тест. Регрессия закрыта навсегда, а не до следующего рефакторинга |
| Непрерывная интеграция | Основа конвейера РБПО: проверки запускаются автоматически, а не когда о них вспомнили |
| Стандарт кодирования | Правила кодирования — прямое требование процесса 5.8 |
Честная оговорка. XP в чистом виде сегодня применяют редко. Но непрерывная интеграция, разработка через тестирование и парное программирование ушли из него в общую практику и живут там отдельно от методологии. Это тот случай, когда подход растворился в отрасли — и это лучшее, что может случиться с методологией.
Отдельно стоит сказать про 40-часовую неделю. В списке практик она выглядит неуместно — при чём тут инженерия? При том, что усталый разработчик даёт больше дефектов, а дефект, найденный на проде, стоит в сотни раз дороже переработки, которая его породила. Устойчивый темп — это тоже мера безопасности, просто она измеряется не в коде.
Десять слоёв современной разработки
Когда говорят «стек технологий», имеют в виду набор выборов — по одному на каждом слое. Слоёв примерно десять.
Язык и среда выполнения
Python, TypeScript, Java, C#, Go, Rust, Kotlin, Swift
Пакеты и изолированное окружение
pip / uv, npm / pnpm, NuGet, Maven, Cargo, Go modules
Фреймворк
Django, FastAPI, React, Vue, Spring Boot, ASP.NET Core
Хранилище данных
PostgreSQL, SQLite, Redis, ClickHouse, MongoDB
Упаковка и перенос
Docker, Docker Compose, Kubernetes
Контроль версий
Git + GitLab, GitVerse, GitFlic, Gitea. Предмет процесса 5.4
CI/CD
GitLab CI, GitHub Actions, Jenkins, TeamCity. Здесь живёт конвейер РБПО
Качество и безопасность
Линтер, тесты, SAST, SCA, DAST, фаззинг. Процессы 5.10, 5.11, 5.16, 5.18
Наблюдаемость
Логи, метрики, трассировки. Как понять, что происходит в работе
Помощь разработчику
IDE, отладчик, ИИ-ассистенты
Граница модели. Схема из 10 слоёв — авторская дидактическая модель, а не структура стандарта и не статистика учебных программ. Она помогает увидеть, что выпуск требует не только кода, но и управления версиями, конвейера, проверок и эксплуатации. Точный объём проверки при оценке соответствия определяют применимая схема, требования заказчика и модель угроз продукта; его нельзя свести к фиксированным слоям 6–8.
Где какой язык живёт
Вопрос «какой язык лучше» не имеет ответа. Вопрос «какой язык для какой задачи» — имеет.
| Язык | Основные ниши | Типичные фреймворки |
|---|---|---|
| Python | Веб-бэкенд, анализ данных и ML, автоматизация, скрипты ИБ | Django, FastAPI, Flask |
| JavaScript / TypeScript | Веб-интерфейсы, серверная часть | React, Vue, Svelte, Node.js |
| Java | Крупные корпоративные системы, Android | Spring 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С | Учётные системы, в России очень широко | платформа 1С:Предприятие |
| Object Pascal (Delphi) | Сопровождение унаследованных систем, быстрая разработка интерфейсов | VCL, Lazarus |
| Ассемблер | Обратная разработка, загрузчики, оптимизация узких мест | — |
Расписание сегодняшнего дня называет четыре языка: Assembler, Python, Delphi, C#. Выбор не случайный — это четыре разных уровня отношений с памятью, и в разделе 10 мы посмотрим на них на одной и той же задаче.
Библиотека, фреймворк, платформа
Различие определяется одним признаком — кто кого вызывает.
Библиотеку вызываете вы
Вы решаете, когда обратиться к requests, чтобы отправить запрос. Управление остаётся у вас.
Фреймворк вызывает вас
Django сам решает, когда вызвать вашу функцию обработки страницы. Это называется инверсией управления.
Платформа — .NET, JVM, Docker — определяет, где и как вы вообще можете быть вызваны. Она задаёт правила игры до того, как в неё вступит и ваш код, и фреймворк.
Почему это важно для безопасности. Когда чужой код управляет вашим, уязвимость в нём становится вашей уязвимостью — и вы о ней не узнаете, пока специально не начнёте следить. Отсюда композиционный анализ (процесс 5.16) и SBOM: перечень всех компонентов, входящих в продукт. Без него на вопрос «а у вас есть эта уязвимая библиотека?» приходится отвечать «сейчас поищем», и поиск занимает недели.
Кто ещё участвует в разработке
Часть кода сегодня создаётся с помощью ИИ-ассистентов, и это уже обычная практика, а не экзотика. Для РБПО важны три вещи.
Ответственность не делится
- Отвечает тот, кто отправил код в репозиторий
- Сгенерированный код проходит те же процессы 5.8–5.11
- «Так сгенерировал ассистент» — не объяснение для проверяющего
Объём растёт быстрее внимания
- Кода становится больше, а глаз — столько же
- Это усиливает роль автоматических проверок
- Ручное ревью перестаёт масштабироваться первым
Новый риск цепочки поставок
- Ассистент может предложить несуществующий пакет
- Злоумышленник может заранее занять это имя
- Проверка имён зависимостей стала обязательной
Практическое правило. Пользоваться ассистентами можно, и в этом нет ничего плохого. Но не отправляйте в коммит строку, которую не можете объяснить. Как только вы нажали «закоммитить» — это ваш код и ваша ответственность.
Задача дня: журнал учёта носителей
Приложение намеренно маленькое. Всё интересное сегодня — не в нём, а вокруг него.
Учётная запись: инвентарный номер, тип носителя, гриф, ответственный.
Функции: добавить носитель, найти по номеру или ФИО, выгрузить отчёт в файл.
Требования безопасности через «никогда»
Формулировка через «никогда» — не стилистический приём. Такое требование почти механически превращается в тест, и мы это увидим в разделе 13.
- Поиск для обычного пользователя никогда не возвращает записи с грифом «Конфиденциально».
- Отчёт никогда не записывается за пределы каталога выгрузки.
- Служебный токен никогда не хранится в исходном коде.
- Служебный токен никогда не попадает в журнал приложения.
- Запись с невалидными данными никогда не попадает в журнал.
Мини-модель угроз
| Актив | Угроза | Кто | Мера |
|---|---|---|---|
| Перечень конфиденциальных носителей | Раскрытие через функцию поиска | Внутренний пользователь без прав | Фильтр по грифу + регрессионный тест |
| Файлы за пределами каталога | Перезапись произвольного файла | Внутренний пользователь | Нормализация пути и проверка границы |
| Служебный токен | Компрометация через репозиторий | Любой с доступом к git | Секрет вне кода, .gitignore |
| Служебный токен | Компрометация через журналы | Оператор, служба поддержки | Маскирование при записи в журнал |
| Целостность журнала учёта | Внесение мусорных записей | Пользователь, ошибка интеграции | Валидация в конструкторе объекта |
Запомните это место. Пять строк «никогда» и пять строк модели угроз, написанные за 15 минут в начале дня, к вечеру превратятся в пять тестов, которые будут проверяться автоматически при каждом изменении кода. Это и есть сдвиг влево в самом дешёвом исполнении.
От словаря к классу
Первая версия журнала хранит записи в словарях. Она работает. И в ней уже три дефекта.
record = {"inv": "МНИ-001", "kind": "USB-флеш",
"label": "ДСП", "owner": "Иванова А. П."}
| Проблема | Что произойдёт |
|---|---|
| У словаря нет схемы | Опечатка record["Owner"] вместо record["owner"] не вызовет ошибки — просто создастся новый ключ, и данные молча разойдутся |
| Нет проверки значений | Тип «Дискета», гриф «Совершенно секретно», ФИО «asdf» — всё пройдёт без единого возражения |
| Нет защиты от дублей | Один инвентарный номер можно добавить дважды |
@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 = ("Открыто", "ДСП", "Конфиденциально")
Причина простая: перечислить всё разрешённое реально. Перечислить всё запрещённое — нет. Всегда найдётся способ записать то же самое иначе, и этот способ обнаружит не тот, кто пишет фильтр.
Четыре языка, одна задача
Задача: скопировать строку в буфер размером ровно 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-инъекции, от ошибки в логике доступа и от секрета, попавшего в репозиторий. Именно их мы сейчас и пойдём ловить.
Четыре дефекта безопасности
Все четыре живут в файле 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") и сравниваются байты. Хорошая иллюстрация того, что «правильная» функция тоже требует чтения документации.
Чего не лечит выбор языка
Все четыре дефекта из предыдущего раздела воспроизводятся на любом языке — на C#, на Java, на Go. Memory safety тут не помогает вообще.
| Дефект | CWE | Категория OWASP Top 10 (ред. 2021) |
|---|---|---|
| Д3 · Внедрение SQL-кода | CWE-89 | A03 · Injection |
| Д4 · Выход за пределы каталога | CWE-22 | A01 · Broken Access Control |
| Д1 · Секрет в исходном коде | CWE-798 | A07 · Identification and Authentication Failures |
| Д2 · Секрет в журнале | CWE-532 | A09 · Security Logging and Monitoring Failures |
| Переполнение буфера (раздел 10) | CWE-787 | вне веб-контекста OWASP |
Три класса дефектов, от которых язык не спасает
Внедрение кода
- SQL, команды оболочки, пути, шаблоны, LDAP
- Ошибка построения команды из данных
- Возможна на любом языке без исключений
Дефекты логики доступа
- Программа корректно выполняет неправильное правило
- Компилятор здесь бессилен по определению
- Ловится только тестами на требования
Секреты и цепочка поставок
- Организационно-процессная проблема
- К языку отношения не имеет вообще
- Лечится дисциплиной и автоматикой
Что из этого следует для РБПО. Именно поэтому ГОСТ Р 56939-2024 описывает 25 процессов, а не один пункт «пишите на безопасном языке». Безопасный язык закрывает один класс дефектов — важный, но один. Остальные закрываются требованиями, архитектурой, правилами кодирования, экспертизой кода, тестами, статическим и динамическим анализом, управлением конфигурацией и работой с уязвимостями.
Тесты и регрессии безопасности
Два вида тестов, и разница между ними — главная мысль последней пары.
Функциональные
Проверяют, что программа делает то, что должна. Добавляет запись, находит по фамилии, считает количество.
Регрессии безопасности
Проверяют, что программа не делает того, чего не должна. Это память об уже найденной уязвимости.
Пока регрессионный тест зелёный, дефект не может вернуться в код незамеченным. Это процесс 5.18 ГОСТ Р 56939-2024 в самом дешёвом исполнении: одна функция вместо целого регламента.
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)
Тесты падают — значит, они действительно проверяют то, что заявлено. Только теперь им можно верить.
Линтер, SAST, SCA — разные инструменты
Их часто валят в одну кучу под словом «анализаторы». Это разные инструменты для разных задач, и подмена одного другим — типичная ошибка при внедрении.
$ 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) записаны как четыре разных процесса, а не один.
Конвейер и релиз
Конвейер РБПО — это те же самые команды, которые вы вводили руками, только запускаемые автоматически при каждом изменении кода.
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 |
| Известные ограничения | Нет аутентификации пользователей, нет разграничения доступа, нет шифрования хранилища |
Последняя строка обязательна. Честный перечень того, чего в продукте нет, — признак зрелого процесса, а не слабости. Продукт без списка ограничений означает одно из двух: либо его не анализировали, либо список есть, но его прячут. Оба варианта хуже, чем честная строка.
Ответ на вопрос дня
Чем «программа работает» отличается от «программу можно выпускать»?
Разницей в проверяемых свидетельствах. Работающий код — это одно свидетельство. Вот полный список:
- Сформулированные требования, в том числе требования безопасности через «никогда»
- Модель угроз — пусть на пять строк, но написанная
- Контроль ввода, встроенный в структуру данных, а не приделанный сбоку
- Работающий код — то самое единственное свидетельство, с которого все начинают
- Функциональные тесты
- Регрессионные тесты безопасности, проверенные на дефектной версии
- Отчёт линтера
- Отчёт анализатора безопасности
- Отчёт композиционного анализа зависимостей
- История изменений в системе контроля версий
- Отсутствие секретов в коде и в истории репозитория
- Воспроизводимая сборка с зафиксированными параметрами компиляции
- Зафиксированная версия выпуска
- Честный перечень того, чего в продукте нет
И главное. Многие свидетельства можно полностью или частично собирать и проверять автоматически. Требования, модель угроз, решения об исключениях и финальное принятие риска требуют ответственных людей. Сдвиг влево сокращает обратную связь, но не отменяет контроль перед выпуском и в эксплуатации.
Связанные учебные модули
| Модуль | Тема | Связь с этой лекцией |
|---|---|---|
| Автоматизация | Тестирование ПО, CI/CD, GitLab CE | Продолжение разделов 13 и 15 |
| Языки и архитектура | Хроника языков, декларативные языки (Prolog, SQL), сдвиг влево, фреймворки, архитектура | Продолжение разделов 5, 6 и 10. Здесь SQL встретился как источник уязвимости; в связанном модуле он разбирается как язык |
| Конвейер РБПО | Статический, композиционный и динамический анализ, фаззинг | Продолжение раздела 14 вглубь |
| Итоговый модуль | Сведение свидетельств и итоговая проверка | Применение полного набора критериев выпуска |
Скачать все материалы одним архивом
В архиве — эта лекция целиком, конспект, практические задания и весь учебный код. Лекция внутри архива открывается без интернета, и ссылки на файлы комплекта — конспект, задания, код — из неё работают. Ссылки на внешние источники, разумеется, требуют интернета. Разбирать и программировать можно дома, в поезде и на даче.
Что внутри и с чего начать. Распакуйте архив и откройте ЧИТАТЬ-ПЕРВЫМ.md — там порядок работы. Лекция — index.html, конспект и задания — в каталоге materials/, весь код — в code/. Учебное приложение работает на стандартной библиотеке Python: ставить дополнительно ничего не нужно.
Что почитать
- ГОСТ Р 56939-2024 — выборочно по процессам сегодняшнего дня: 5.8, 5.9, 5.10, 5.12, 5.18
- Банк данных угроз ФСТЭК России — bdu.fstec.ru
- CWE — cwe.mitre.org, поиск по номерам занятия: CWE-89, CWE-22, CWE-798, CWE-532, CWE-787
- OWASP Top 10 — перечень наиболее критичных рисков веб-приложений
- Kent Beck. Extreme Programming Explained — первоисточник по XP