М БРПО · Тема № 4 · универсальный учебный модуль

Языки, архитектура и безопасный SQL

Как выбор языка изменяет классы возможных ошибок, почему безопасность должна появляться в требованиях и архитектуре, и как отделить данные от SQL-кода.

Формат 2 лекции + 3 практических занятия
Сквозная задача Реестр сторонних компонентов ПО
Среда Python 3.11+ · SQLite · без runtime-зависимостей

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

01 Вопрос дня

Где именно выбор языка становится решением по безопасности?

Не в рейтинге популярности и не в споре «компилируемый против интерпретируемого», а в архитектурном решении: какие ошибки возможны, где проходят границы доверия и какими средствами команда докажет выполнение требований.

2
лекционных блока: язык и архитектура
3
SQL-практикума по 90 минут
19
автоматических проверок готовых примеров и результата
1
изолированная демонстрация CWE-89

К вечеру слушатель должен уметь

  • сравнивать языки по нескольким признакам, а не по одному ярлыку;
  • фиксировать выбор языка и стека как проверяемое архитектурное решение;
  • различать библиотеку, фреймворк, платформу и предметно-ориентированный язык;
  • проектировать SQL-схему с ограничениями целостности;
  • разделять SQL-код и данные с помощью placeholders;
  • применять allowlist там, где идентификаторы нельзя параметризовать;
  • подтверждать результат тестами, SAST и CI, а не только ручным запуском.
02 Лекция 1

Хроника языков: менялась не только запись программ

Каждое поколение языков переносило часть ответственности от разработчика к транслятору, среде выполнения, системе типов или библиотекам. Вместе с удобством перемещалась и поверхность атаки.

1940–1950-е
Машинный код и ассемблер
Полный контроль над памятью и устройством; почти вся корректность — ответственность программиста.
1957–1960
Fortran, Lisp, COBOL, ALGOL
Высокоуровневые конструкции, новые парадигмы, переносимость и формализация алгоритмов.
1969–1973
C и Unix
Системное программирование с переносимостью и прямой работой с памятью.
1979–1985
C with Classes → C++
Абстракции и объектная модель поверх возможностей C.
1989–1991
Python
Высокоуровневая динамическая среда, несколько парадигм и автоматическое управление памятью.
Сегодня
Многоязычные системы
Один продукт сочетает DSL, SQL, C/C++, Python, JavaScript, конфигурации и код инфраструктуры.

Историческая ось не является рейтингом безопасности. Новый язык может исключить часть старых ошибок, но добавить сложную среду выполнения, цепочку зависимостей, FFI и новые способы неправильного использования API.

03 Несколько осей

«Компилируемый» и «интерпретируемый» — недостаточно

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

В конспекте зафиксированы четыре независимые оси классификации — уровень абстракции, парадигма, модель выполнения и область применения. Таблица ниже повторяет их и добавляет инженерные срезы (типизация, память, FFI, toolchain), которые понадобятся при выборе стека.

Ось сравненияВопросСвязь с безопасностью
Уровень абстракцииНасколько явно код управляет памятью и устройством?Классы ошибок памяти, контроль ресурсов, детерминизм.
ПарадигмыИмперативная, объектная, функциональная, декларативная?Способы выражения инвариантов и разделения ответственности.
ТипизацияСтатическая или динамическая; строгая или допускающая неявные преобразования?Какие ошибки ловятся до запуска, а какие только во время выполнения.
ПамятьРучная, автоматическая, владение и заимствование?Use-after-free, переполнения, утечки, паузы сборщика мусора.
Модель исполненияНативный код, виртуальная машина, JIT, интерпретатор?Hardening, sandbox, поверхность среды выполнения.
FFI и unsafeГде высокоуровневый код пересекается с нативным?Возврат рисков памяти на границе доверия.
Область примененияУниверсальный язык или предметно-ориентированный (DSL)?Широта поверхности атаки и специализация проверок.
ToolchainЕсть ли SAST, sanitizers, fuzzing, lock-файлы, SBOM?Способность обнаруживать и воспроизводимо устранять дефекты.

Императивный подход

Программа задаёт последовательность изменения состояния: «как получить результат».

CPythonPascal

Декларативный подход

Программа описывает требуемый результат или ограничения: «что должно быть истинно».

SQLPrologрегулярные выражения
04 Поверхность атаки

Язык исключает одни дефекты, но не отменяет требования

Выбор 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 требуют процесса применения и управления отклонениями; один запуск анализатора не превращает код в безопасный.

05 Предметная область

DSL: меньше свободы, больше смысла

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

1

Python вызывает драйвер

Императивный код управляет соединением, ошибками и транзакцией.

2

SQL описывает результат

Декларативный запрос передаётся отдельно от значений.

3

SQLite строит план

Движок анализирует SQL, применяет ограничения и изменяет файл БД.

4

ОС защищает файл

Права файловой системы остаются частью модели доступа.

Ошибка на границе языков. Если приложение склеивает пользовательскую строку с SQL, данные превращаются в программу. Именно это и есть корень CWE-89.

06 Лекция 2

Shift Left: раньше, но не один раз

Требования безопасности, моделирование угроз, анализ архитектуры и автоматические проверки включаются раньше в существующий SDLC. Это не означает перенос всей безопасности только в начало проекта.

зона Shift Left дефект дёшево предотвратить стоимость исправления ×1 ×5 ×10 ×15 ×30+ Shift Left: меры принимаются раньше Требования Архитектура Реализация Верификация Эксплуатация
Соотношения условны (по мотивам классических оценок Барри Боэма и IBM Systems Sciences Institute), но порядок устойчив: чем позже обнаружен дефект, тем дороже исправление. Пять шагов ниже переносят решения безопасности в левую часть оси — не отменяя проверок справа.
01

Требования

Определить активы, недопустимые события и критерии приёмки.

до кода
02

Архитектура

Провести threat modeling, определить границы доверия, язык, компоненты и компенсирующие меры.

до зависимости
03

Реализация

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

при изменении
04

Верификация

Тесты, review, SAST, SCA, DAST и fuzzing выполняются автоматически и повторяемо.

каждая сборка
05

Эксплуатация

Следить за уязвимостями, обновлять решения и предотвращать повторение первопричин.

весь срок жизни

Практический признак Shift Left сегодня: ограничение CHECK в схеме, placeholder в API и тест на инъекционный payload появляются до того, как код назван готовым.

07 Прослеживаемость

От инженерной практики к проверяемому процессу

Решение занятияГОСТ Р 56939-2024NIST SSDF v1.1Свидетельство
Выбор языка и структура решения5.6 АрхитектураPW.1, PW.1.2ADR и схема границ доверия
Инъекция как сценарий угрозы5.7 Моделирование угрозPW.1.1Сценарий, актив, мера и тест
Placeholder и allowlist5.8 Правила кодированияPW.5.1Правило + проверяемый пример
Автоматический анализ5.10 Статический анализPW.7 / PW.8Лог CI и разбор находок
Регрессионные тесты5.18 Функциональное тестированиеPW.819 повторяемых тестов комплекта
Проверка компонентов5.16 Композиционный анализPW.4.1, PS.3.2Перечень зависимостей, SBOM, отчёт SCA

Граница утверждения. Карточка Росстандарта подтверждает статус и область применения стандарта. Для формального решения по конкретному подпункту используют легальный экземпляр полного текста и внутреннюю матрицу «требование → регламент → артефакт».

08 Чужой код

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

A

Библиотека

Приложение подключает функциональность и обычно само определяет момент вызова.

B

Фреймворк

Задаёт жизненный цикл и вызывает прикладной код в предусмотренных точках расширения.

C

Платформа

Может включать runtime, библиотеки, SDK, компилятор, инструменты и стеки приложений.

D

Движок

Готовая среда исполнения предметной области; часто сочетает признаки платформы и фреймворка.

Фраза «библиотеку вызываете вы, фреймворк вызывает вас» полезна как эвристика, но реальные продукты бывают гибридными. Для безопасности название не меняет обязательных действий.

Библиотека Ваш код вызываете вы Библиотека момент вызова выбирает приложение Фреймворк Фреймворк вызывает вас Ваш код точки расширения задаёт каркас
Главное различие — направление вызова и, вместе с ним, контроль над жизненным циклом: у библиотеки его сохраняет ваш код, во фреймворке — каркас. Обязательные действия безопасности из списка ниже одинаковы для обоих случаев.
  • идентифицировать компонент, версию, поставщика и источник;
  • проверить происхождение, целостность и лицензию;
  • найти известные уязвимости и оценить достижимость;
  • зафиксировать компонент в SBOM и политике допуска;
  • назначить владельца обновления и срок поддержки;
  • учесть расширения, плагины и транзитивные зависимости.
09 Security by Design

ADR выбора языка и стека

Архитектурное решение должно объяснять не только «что выбрали», но и какие угрозы учтены, какие альтернативы отклонены и чем контролируется остаточный риск.

1Контекст и активы

Какие данные и функции защищаются? Где проходят доверительные границы? Каковы последствия отказа, раскрытия или подмены?

2Ограничения

Нужны ли жёсткое реальное время, детерминизм, сертифицированный компилятор, ограниченная память или прямой доступ к оборудованию?

3Риски реализации

Какие классы CWE возможны? Каков объём unsafe/FFI? Поддерживает ли toolchain hardening, SAST, sanitizers, fuzzing и воспроизводимую сборку?

4Цепочка поставок

Как фиксируются зависимости, provenance, лицензии, SBOM, обновления и срок поддержки выбранной реализации?

5Решение и доказательства

Какие альтернативы рассмотрены? Какие меры обязательны? Кто независимо проверил решение и когда его пересмотреть?

10 Сквозная задача

Модель угроз реестра компонентов

Учебное приложение хранит наименование компонента, версию, SPDX-идентификатор лицензии, поставщика и критичность. Данные вымышлены, но угрозы типовые.

граница доверия 1 граница доверия 2 граница доверия 3 Пользователь, другой клиент возможен любой ввод, включая payload Python- приложение валидация значений allowlist идентификаторов транзакции Движок SQLite план запроса CHECK · UNIQUE · FK STRICT-типизация Файл components.db права ОС mode=ro для читателя строка ввода два раздельных канала: SQL-текст с ? значения параметров запись в файл
Схема границ доверия учебного реестра. Каждую границу пересекает только узкий проверяемый канал; ключевая деталь — граница 2, где текст запроса с placeholder и значения параметров идут раздельно, поэтому данные не могут изменить команду.
Актив / границаНедопустимое событиеРанняя мераПроверка
SQL-запросВвод изменяет структуру командыPlaceholder для каждого значенияPayload хранится как строка
ORDER BYИмя столбца становится произвольным SQLЗакрытый allowlist идентификаторовНеизвестный ключ отклоняется
Связи данныхПоявляются осиротевшие записиForeign key на каждом соединенииНарушение связи блокируется
Изменение лицензииОбновление и аудит расходятсяОдна транзакцияОшибка откатывает обе операции
Файл БДЧитающий процесс меняет данныеСоединение mode=ro и права ОСINSERT завершается ошибкой
КомпонентДанные вне допустимого доменаNOT NULL, CHECK, UNIQUEНевалидная строка не сохраняется
11 ПЗ-1 · 13:30

Безопасность начинается со схемы

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

schema.sql (фрагмент)
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 само по себе не доказывает, что он проверяется в конкретном подключении.

12 ПЗ-2 · 15:10

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 остаётся строкой. Ручное «экранирование кавычек» не заменяет параметризацию.

Ввод пользователя ' OR 1=1 -- одна и та же строка Путь 1 · склейка строк Склейка f-строкой f"WHERE name = '{term}'" WHERE name = '' OR 1=1 --' SQLite разбирает команду ввод стал частью кода: условие 1=1 всегда истинно вернулись все 3 строки Путь 2 · параметризация Placeholder и кортеж "WHERE name = ?" ("' OR 1=1 --",) SQLite разбирает команду код и значение пришли раздельно: payload — просто текст для поиска вернулось 0 строк
Судьба одной и той же строки на двух путях. Именно это показывает demo_unsafe_query.py: небезопасный запрос возвращает все 3 учебные записи, параметризованный — ни одной, потому что компонента с таким именем не существует.

Демонстрация изолирована. demo_unsafe_query.py работает только с тремя вымышленными строками в памяти и использует фиксированный учебный payload. Уязвимая функция не является заготовкой для проекта.

13 Значения и идентификаторы

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Булево значение → две константыПроизвольная строка направления
LIKEPlaceholder + определённая политика для % и _Неясно, считаются ли wildcard данными
LIMITПараметр и верхняя границаНеограниченная выгрузка
14 ПЗ-3 · 16:50

Транзакция, аудит и минимальные полномочия

1

Проверить

Тип, длина, допустимый домен и существование объекта.

2

Изменить

Выполнить UPDATE с placeholders.

3

Зафиксировать

Добавить событие аудита в той же транзакции.

4

Откатить

При ошибке не оставлять половину результата.

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: транзакцию начнёт первая запись одна транзакция — одно целое UPDATE components INSERT audit_events без исключений COMMIT видны обе записи исключение ROLLBACK не видна ни одна
Два исхода блока with conn: — и третьего не дано: «половина результата», когда лицензия изменена, а событие аудита потеряно, невозможна по построению. Сам оператор with транзакцию не открывает — её начинает первая изменяющая команда, а блок на выходе выполняет COMMIT или ROLLBACK. Атомарность проверяет тест test_change_license_rolls_back_when_component_is_missing.

Для читающего процесса используется отдельное подключение file:...?...mode=ro. В рабочей клиент-серверной СУБД принцип продолжается отдельной учётной записью с минимальными правами. Только приложение не должно решать, может ли оно писать: запрет должен обеспечивать и нижний слой.

Аудит в той же БД — учебное упрощение. Пользователь с правом менять файл способен изменить и журнал. Для независимого аудита нужны отдельный контур хранения, защита целостности, синхронизация времени и контроль доступа.

15 Работа слушателя

Маршрут трёх практических занятий

ПЗ-1

Схема и данные

Прочитать schema.sql, создать components.db, проверить ограничения и сформулировать инварианты.

13:30–15:00
ПЗ-2

CWE-89 и безопасные запросы

Сравнить фиксированную демонстрацию с параметризацией, разобрать LIKE, ORDER BY и верхнюю границу LIMIT.

15:10–16:40
ПЗ-3

RED → GREEN → quality gate

Реализовать пять функций в step3_student.py, получить 13 зелёных тестов и разобрать CI.

16:50–18:20

Команды

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

Подробный порядок без ответов находится в практикуме; там же есть раздел «Если что-то пошло не так» с разбором типичных ошибок. Эталон реализации и ключи не входят в студенческий архив.

16 Доказательства

CI проверяет именно студенческий результат

стадия lint стадия test · один job стадия security · параллельные job ruff стиль и ошибки job lint test_examples примеры 6 тестов test_student ваш результат 14 тестов bandit job sast · SAST pip-audit job sca · SCA результат подтверждён любая красная проверка останавливает конвейер до исправления результат не считается готовым
Конвейер учебного quality gate из .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 — ожидаемое и честное состояние.

17 Итог

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

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

  • язык влияет на возможные ошибки памяти, типизации и границ FFI;
  • архитектура определяет доверительные границы и полномочия компонентов;
  • фреймворк и платформа расширяют цепочку поставок и поверхность обновления;
  • схема SQL превращает часть требований в исполняемые ограничения;
  • placeholder отделяет данные от кода, но не заменяет allowlist идентификаторов;
  • тесты и CI превращают правила в повторяемые свидетельства;
  • Shift Left означает раннюю и постоянную интеграцию безопасности во весь SDLC.

Скачать комплект

Архив содержит офлайн-лекцию, конспект, задания и учебный код. Для основных упражнений достаточно Python 3.11 или новее; SQLite и unittest входят в стандартную библиотеку.

Скачать материалы занятия (ZIP) Откройте внутри ЧИТАТЬ-ПЕРВЫМ.md

Первичные источники