Языки, архитектура и безопасный SQL
Как выбор языка изменяет классы возможных ошибок, почему безопасность должна появляться в требованиях и архитектуре, и как отделить данные от SQL-кода.
Материалы занятия
Где именно выбор языка становится решением по безопасности?
Не в рейтинге популярности и не в споре «компилируемый против интерпретируемого», а в архитектурном решении: какие ошибки возможны, где проходят границы доверия и какими средствами команда докажет выполнение требований.
К вечеру слушатель должен уметь
- сравнивать языки по нескольким признакам, а не по одному ярлыку;
- фиксировать выбор языка и стека как проверяемое архитектурное решение;
- различать библиотеку, фреймворк, платформу и предметно-ориентированный язык;
- проектировать SQL-схему с ограничениями целостности;
- разделять SQL-код и данные с помощью placeholders;
- применять allowlist там, где идентификаторы нельзя параметризовать;
- подтверждать результат тестами, SAST и CI, а не только ручным запуском.
Хроника языков: менялась не только запись программ
Каждое поколение языков переносило часть ответственности от разработчика к транслятору, среде выполнения, системе типов или библиотекам. Вместе с удобством перемещалась и поверхность атаки.
Историческая ось не является рейтингом безопасности. Новый язык может исключить часть старых ошибок, но добавить сложную среду выполнения, цепочку зависимостей, FFI и новые способы неправильного использования API.
«Компилируемый» и «интерпретируемый» — недостаточно
Современный язык нужно рассматривать вместе с его реализацией и экосистемой. CPython, например, получает байткод и исполняет его виртуальной машиной; поэтому граница между компиляцией и интерпретацией размыта.
В конспекте зафиксированы четыре независимые оси классификации — уровень абстракции, парадигма, модель выполнения и область применения. Таблица ниже повторяет их и добавляет инженерные срезы (типизация, память, FFI, toolchain), которые понадобятся при выборе стека.
| Ось сравнения | Вопрос | Связь с безопасностью |
|---|---|---|
| Уровень абстракции | Насколько явно код управляет памятью и устройством? | Классы ошибок памяти, контроль ресурсов, детерминизм. |
| Парадигмы | Императивная, объектная, функциональная, декларативная? | Способы выражения инвариантов и разделения ответственности. |
| Типизация | Статическая или динамическая; строгая или допускающая неявные преобразования? | Какие ошибки ловятся до запуска, а какие только во время выполнения. |
| Память | Ручная, автоматическая, владение и заимствование? | Use-after-free, переполнения, утечки, паузы сборщика мусора. |
| Модель исполнения | Нативный код, виртуальная машина, JIT, интерпретатор? | Hardening, sandbox, поверхность среды выполнения. |
| FFI и unsafe | Где высокоуровневый код пересекается с нативным? | Возврат рисков памяти на границе доверия. |
| Область применения | Универсальный язык или предметно-ориентированный (DSL)? | Широта поверхности атаки и специализация проверок. |
| Toolchain | Есть ли SAST, sanitizers, fuzzing, lock-файлы, SBOM? | Способность обнаруживать и воспроизводимо устранять дефекты. |
Императивный подход
Программа задаёт последовательность изменения состояния: «как получить результат».
Декларативный подход
Программа описывает требуемый результат или ограничения: «что должно быть истинно».
Язык исключает одни дефекты, но не отменяет требования
Выбор memory-safe языка способен устранить или сильно затруднить часть ошибок ручного управления памятью. Он не устраняет ошибки авторизации, SQL-инъекции, небезопасную десериализацию, секреты в коде и уязвимые зависимости.
C/C++
- прямой контроль памяти и аппаратуры;
- подходит для систем с жёсткими ограничениями;
- требует compiler hardening, sanitizers, SAST, fuzzing и строгих правил;
- CWE-119 описывает широкий класс ошибок границ буфера.
Управляемая среда
- часть ошибок памяти предотвращает runtime;
- сохраняются логические и инъекционные дефекты;
- нативные расширения возвращают риски FFI;
- компрометация платформы или зависимости влияет на всё приложение.
Минимум для C/C++ в архитектурном решении
- обосновать, почему требования нельзя закрыть memory-safe альтернативой;
- минимизировать объём небезопасного кода и изолировать его интерфейс;
- включить
-fstack-protector-strong,-D_FORTIFY_SOURCE=3, PIE/RELRO там, где поддерживается; - на тестовых сборках применять ASan/UBSan, а при многопоточности — TSan отдельным прогоном;
- зафиксировать правила CERT C/C++ или применимый профиль MISRA и процесс управления отклонениями;
- добавить fuzzing и регрессионный тест на каждый подтверждённый дефект.
Соответствие правилам не равно отсутствию уязвимостей. CERT и MISRA требуют процесса применения и управления отклонениями; один запуск анализатора не превращает код в безопасный.
DSL: меньше свободы, больше смысла
Предметно-ориентированный язык выражает задачи конкретной области. SQL описывает выборку и изменение отношений; регулярные выражения — шаблоны строк; конфигурационный DSL — состояние инфраструктуры. Ограниченный словарь может сделать намерение проверяемым, но интерпретатор DSL остаётся границей доверия.
Python вызывает драйвер
Императивный код управляет соединением, ошибками и транзакцией.
SQL описывает результат
Декларативный запрос передаётся отдельно от значений.
SQLite строит план
Движок анализирует SQL, применяет ограничения и изменяет файл БД.
ОС защищает файл
Права файловой системы остаются частью модели доступа.
Ошибка на границе языков. Если приложение склеивает пользовательскую строку с SQL, данные превращаются в программу. Именно это и есть корень CWE-89.
Shift Left: раньше, но не один раз
Требования безопасности, моделирование угроз, анализ архитектуры и автоматические проверки включаются раньше в существующий SDLC. Это не означает перенос всей безопасности только в начало проекта.
Требования
Определить активы, недопустимые события и критерии приёмки.
Архитектура
Провести threat modeling, определить границы доверия, язык, компоненты и компенсирующие меры.
Реализация
Применить правила языка, безопасные API и ограничения данных.
Верификация
Тесты, review, SAST, SCA, DAST и fuzzing выполняются автоматически и повторяемо.
Эксплуатация
Следить за уязвимостями, обновлять решения и предотвращать повторение первопричин.
Практический признак Shift Left сегодня: ограничение CHECK в схеме, placeholder в API и тест на инъекционный payload появляются до того, как код назван готовым.
От инженерной практики к проверяемому процессу
| Решение занятия | ГОСТ Р 56939-2024 | NIST SSDF v1.1 | Свидетельство |
|---|---|---|---|
| Выбор языка и структура решения | 5.6 Архитектура | PW.1, PW.1.2 | ADR и схема границ доверия |
| Инъекция как сценарий угрозы | 5.7 Моделирование угроз | PW.1.1 | Сценарий, актив, мера и тест |
| Placeholder и allowlist | 5.8 Правила кодирования | PW.5.1 | Правило + проверяемый пример |
| Автоматический анализ | 5.10 Статический анализ | PW.7 / PW.8 | Лог CI и разбор находок |
| Регрессионные тесты | 5.18 Функциональное тестирование | PW.8 | 19 повторяемых тестов комплекта |
| Проверка компонентов | 5.16 Композиционный анализ | PW.4.1, PS.3.2 | Перечень зависимостей, SBOM, отчёт SCA |
Граница утверждения. Карточка Росстандарта подтверждает статус и область применения стандарта. Для формального решения по конкретному подпункту используют легальный экземпляр полного текста и внутреннюю матрицу «требование → регламент → артефакт».
Библиотека, фреймворк и платформа
Библиотека
Приложение подключает функциональность и обычно само определяет момент вызова.
Фреймворк
Задаёт жизненный цикл и вызывает прикладной код в предусмотренных точках расширения.
Платформа
Может включать runtime, библиотеки, SDK, компилятор, инструменты и стеки приложений.
Движок
Готовая среда исполнения предметной области; часто сочетает признаки платформы и фреймворка.
Фраза «библиотеку вызываете вы, фреймворк вызывает вас» полезна как эвристика, но реальные продукты бывают гибридными. Для безопасности название не меняет обязательных действий.
- идентифицировать компонент, версию, поставщика и источник;
- проверить происхождение, целостность и лицензию;
- найти известные уязвимости и оценить достижимость;
- зафиксировать компонент в SBOM и политике допуска;
- назначить владельца обновления и срок поддержки;
- учесть расширения, плагины и транзитивные зависимости.
ADR выбора языка и стека
Архитектурное решение должно объяснять не только «что выбрали», но и какие угрозы учтены, какие альтернативы отклонены и чем контролируется остаточный риск.
1Контекст и активы
Какие данные и функции защищаются? Где проходят доверительные границы? Каковы последствия отказа, раскрытия или подмены?
2Ограничения
Нужны ли жёсткое реальное время, детерминизм, сертифицированный компилятор, ограниченная память или прямой доступ к оборудованию?
3Риски реализации
Какие классы CWE возможны? Каков объём unsafe/FFI? Поддерживает ли toolchain hardening, SAST, sanitizers, fuzzing и воспроизводимую сборку?
4Цепочка поставок
Как фиксируются зависимости, provenance, лицензии, SBOM, обновления и срок поддержки выбранной реализации?
5Решение и доказательства
Какие альтернативы рассмотрены? Какие меры обязательны? Кто независимо проверил решение и когда его пересмотреть?
Модель угроз реестра компонентов
Учебное приложение хранит наименование компонента, версию, SPDX-идентификатор лицензии, поставщика и критичность. Данные вымышлены, но угрозы типовые.
| Актив / граница | Недопустимое событие | Ранняя мера | Проверка |
|---|---|---|---|
| SQL-запрос | Ввод изменяет структуру команды | Placeholder для каждого значения | Payload хранится как строка |
| ORDER BY | Имя столбца становится произвольным SQL | Закрытый allowlist идентификаторов | Неизвестный ключ отклоняется |
| Связи данных | Появляются осиротевшие записи | Foreign key на каждом соединении | Нарушение связи блокируется |
| Изменение лицензии | Обновление и аудит расходятся | Одна транзакция | Ошибка откатывает обе операции |
| Файл БД | Читающий процесс меняет данные | Соединение mode=ro и права ОС | INSERT завершается ошибкой |
| Компонент | Данные вне допустимого домена | NOT NULL, CHECK, UNIQUE | Невалидная строка не сохраняется |
Безопасность начинается со схемы
Валидация только в интерфейсе обходится другим клиентом. Ограничения базы данных действуют для каждого пути записи и становятся исполняемой частью требований.
CREATE TABLE components (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
CHECK (length(trim(name)) BETWEEN 1 AND 100),
criticality TEXT NOT NULL
CHECK (criticality IN (
'low', 'medium', 'high'
))
) STRICT;
Что гарантирует движок
- пустое имя не пройдёт;
- критичность ограничена доменом;
- дубли блокирует
UNIQUE; - ссылочную целостность задаёт
FOREIGN KEY; - режим
STRICTсужает допустимые типы.
SQLite-особенность: PRAGMA foreign_keys = ON нужно включать для каждого соединения. Наличие внешнего ключа в DDL само по себе не доказывает, что он проверяется в конкретном подключении.
SQL-инъекция: данные стали кодом
sql = (
"SELECT id, name FROM components "
f"WHERE name = '{term}'"
)
rows = conn.execute(sql).fetchall()
rows = conn.execute(
"""
SELECT id, name
FROM components
WHERE name = ?
""",
(term,),
).fetchall()
В первом примере значение попадает внутрь грамматики SQL. Во втором драйвер передаёт текст запроса и значение раздельно; payload остаётся строкой. Ручное «экранирование кавычек» не заменяет параметризацию.
demo_unsafe_query.py: небезопасный запрос возвращает все 3 учебные записи, параметризованный — ни одной, потому что компонента с таким именем не существует.Демонстрация изолирована. demo_unsafe_query.py работает только с тремя вымышленными строками в памяти и использует фиксированный учебный payload. Уязвимая функция не является заготовкой для проекта.
Placeholder не подставляет имя столбца
Параметры предназначены для значений. Если пользователь выбирает сортировку, имя столбца нельзя передавать как ?. Его отображают на константу, принадлежащую коду.
SORT_COLUMNS = {
"name": "name",
"version": "version",
"license_spdx": "license_spdx",
"criticality": "criticality",
}
try:
column = SORT_COLUMNS[user_choice]
except KeyError:
raise ValueError("Недопустимый ключ сортировки") # контракт: ValueError
sql = f"SELECT ... ORDER BY {column} ASC"
| Элемент запроса | Правильная мера | Ошибка |
|---|---|---|
| Строковое или числовое значение | Placeholder и отдельный параметр | f-string, %, .format() |
| Имя столбца / таблицы | Закрытое отображение из бизнес-ключа | Прямая вставка ввода |
| ASC / DESC | Булево значение → две константы | Произвольная строка направления |
| LIKE | Placeholder + определённая политика для % и _ | Неясно, считаются ли wildcard данными |
| LIMIT | Параметр и верхняя граница | Неограниченная выгрузка |
Транзакция, аудит и минимальные полномочия
Проверить
Тип, длина, допустимый домен и существование объекта.
Изменить
Выполнить UPDATE с placeholders.
Зафиксировать
Добавить событие аудита в той же транзакции.
Откатить
При ошибке не оставлять половину результата.
with conn:
cursor = conn.execute(
"UPDATE components SET license_spdx = ? WHERE id = ?",
(new_license, component_id),
)
if cursor.rowcount != 1:
raise LookupError("Компонент не найден")
conn.execute(
"INSERT INTO audit_events (...) VALUES (?, ?)",
("license_changed", component_id),
)
with conn: — и третьего не дано: «половина результата», когда лицензия изменена, а событие аудита потеряно, невозможна по построению. Сам оператор with транзакцию не открывает — её начинает первая изменяющая команда, а блок на выходе выполняет COMMIT или ROLLBACK. Атомарность проверяет тест test_change_license_rolls_back_when_component_is_missing.Для читающего процесса используется отдельное подключение file:...?...mode=ro. В рабочей клиент-серверной СУБД принцип продолжается отдельной учётной записью с минимальными правами. Только приложение не должно решать, может ли оно писать: запрет должен обеспечивать и нижний слой.
Аудит в той же БД — учебное упрощение. Пользователь с правом менять файл способен изменить и журнал. Для независимого аудита нужны отдельный контур хранения, защита целостности, синхронизация времени и контроль доступа.
Маршрут трёх практических занятий
Схема и данные
Прочитать schema.sql, создать components.db, проверить ограничения и сформулировать инварианты.
CWE-89 и безопасные запросы
Сравнить фиксированную демонстрацию с параметризацией, разобрать LIKE, ORDER BY и верхнюю границу LIMIT.
RED → GREEN → quality gate
Реализовать пять функций в step3_student.py, получить 13 зелёных тестов и разобрать CI.
Команды
py step1_schema.py
py -m unittest -v test_examples.py # служебная проверка примеров, по желанию
py demo_unsafe_query.py
py step2_secure_queries.py
py -m unittest -v test_student.py
Подробный порядок без ответов находится в практикуме; там же есть раздел «Если что-то пошло не так» с разбором типичных ошибок. Эталон реализации и ключи не входят в студенческий архив.
CI проверяет именно студенческий результат
.gitlab-ci.yml: четыре job в трёх стадиях — lint (ruff), единый test (оба набора тестов последовательно), затем параллельные sast (bandit) и sca (pip-audit). До выполнения задания job test красный — это ожидаемое стартовое состояние; соответствие проверок и локальных команд — в таблице ниже.| Шаг | Что проверяет | Результат, который останавливает работу |
|---|---|---|
ruff check . | Ошибки Python, стиль, опасные конструкции выбранных правил | Любая неисправленная находка |
test_examples.py | Готовые примеры ПЗ-1/ПЗ-2 | Не 6/6 |
test_student.py | Параметры, allowlist, транзакция, read-only | Не 14/14 |
bandit -r . -x ./demo_unsafe_query.py,./test_examples.py,./test_student.py | Высокосигнальные опасные конструкции Python; демонстрация и тесты исключены осознанно, рабочий файл слушателя — нет | Необоснованная находка |
pip-audit | Известные уязвимости runtime-зависимостей | Для приложения список пуст; факт фиксируется явно |
В отличие от раннего учебного конвейера, здесь CI по умолчанию запускает именно test_student.py и не исключает рабочий файл слушателя из lint/SAST. До выполнения задания красный pipeline — ожидаемое и честное состояние.
Ответ на вопрос дня
Выбор языка становится решением по безопасности тогда, когда он связан с активами и угрозами, зафиксирован в архитектуре, ограничивает конкретные классы дефектов и сопровождается проверяемым набором мер для оставшихся рисков.
- язык влияет на возможные ошибки памяти, типизации и границ FFI;
- архитектура определяет доверительные границы и полномочия компонентов;
- фреймворк и платформа расширяют цепочку поставок и поверхность обновления;
- схема SQL превращает часть требований в исполняемые ограничения;
- placeholder отделяет данные от кода, но не заменяет allowlist идентификаторов;
- тесты и CI превращают правила в повторяемые свидетельства;
- Shift Left означает раннюю и постоянную интеграцию безопасности во весь SDLC.
Скачать комплект
Архив содержит офлайн-лекцию, конспект, задания и учебный код. Для основных упражнений достаточно Python 3.11 или новее; SQLite и unittest входят в стандартную библиотеку.
Первичные источники
- ГОСТ Р 56939-2024 — официальная карточка Росстандарта
- NIST SP 800-218 v1.1 Secure Software Development Framework
- ISO/IEC 27034-1:2011 Application security
- ISO/IEC 24772-1:2024 Language vulnerabilities
- Dennis Ritchie, The Development of the C Language
- ISO C++ FAQ: история C++
- Python General FAQ
- MITRE CWE-119: Memory Buffer Bounds
- MITRE CWE-89: SQL Injection
- OWASP SQL Injection Prevention Cheat Sheet
- Python
sqlite3: placeholders и транзакции - SQLite Security и Foreign Keys