{ "index": 178, "slug": "editorial-2023-01-field-threat-model", "title": "Модель угроз для одного потока: от симптома до проверяемого контроля", "excerpt": "Как разобрать security-sensitive изменение, когда команда предлагает контроль, но не называет актив, границу и злоупотребление. На примере одного API-потока — схема, код, таблица диагностики и критерий готовности.", "contentHtml": "
В ревью появляется знакомая фраза: «давайте добавим подпись», «закроем endpoint» или «поставим ограничение». Но никто не может ответить, какие данные защищаются и какой запрос должен быть отклонён. Команда выбирает технологию до того, как описывает угрозу. Ошибка стоит дороже лишней строки кода: контроль может сломать легитимный поток, не закрыть нужное злоупотребление и оставить владельца без доказательства результата.
\nРазберём один поток, а не всю систему. На выходе должна получиться не красивая диаграмма, а связка: актив, актор, граница доверия, злоупотребление, контроль, проверка и откат. Пример ниже учебный: в нём нет реального сервиса, ключа или пользовательских данных. Поэтому каждый вывод будет иметь границу применимости.
\nНачните с наблюдаемого факта. Например, обработчик принимает запрос на изменение заявки, но контракт не говорит, какое условие должно остановить неподтверждённый вход до записи. Это ещё не доказательство уязвимости: другой слой может выполнить проверку раньше. Но это достаточная причина найти владельца решения и предъявить отрицательный тест.
\nЗапишите цену ошибки двумя предложениями. Если проверка действительно отсутствует, внешний клиент может попытаться изменить чужую заявку. Если поставить слишком общий запрет, легитимные изменения начнут получать отказы, а команда создаст ручной обход. В обоих случаях спор о подписи преждевременен: сначала надо назвать объект защиты и допустимое поведение.
\nДля учебного потока зададим следующие значения: browser-client отправляет запрос в public-api, обработчик проверяет вход и пишет change-request в хранилище. Актив — не абстрактные «данные», а состояние конкретной заявки и её целостность. Актор — внешний клиент, который может сформировать запрос. Злоупотребление — попытка изменить заявку без подтверждения и без права на эту операцию.
browser-client\n |\n | POST /v1/change-requests/42\n v\n public-api <-- trust boundary: public input / handler decision\n |\n v\n handler ----> change-request store (asset: integrity)\n\nabuse: внешний запрос меняет чужую заявку\ncontrol: проверка подлинности и полномочий до записи\nevidence: отрицательный тест показывает отказ и неизменное состояние\nСлово «подпись» здесь не является готовым решением. Целостность сообщения, аутентификация отправителя и право изменить заявку — разные свойства. Подписанный запрос не становится автоматически разрешённым для любого пользователя; шифрование канала тоже не заменяет авторизацию. Это различие определяет, какие поля и тесты потребуются в реальном проекте.
\nOWASP предлагает начинать с четырёх вопросов: что мы строим, что может пойти не так, что будем с этим делать и достаточно ли хорошо проверили результат. Для одного API-потока их удобно превратить в поля записи. Поля не доказывают безопасность, зато делают пропуск видимым и дают команде общий словарь.
\n| Поле | Вопрос | Пример | Что проверять |
|---|---|---|---|
| Asset | Что нельзя потерять? | Целостность заявки 42 | Какое состояние меняется и кто им владеет |
| Actor | Кто формирует вход? | Внешний клиент | Какие у него права и какие данные ему доступны |
| Boundary | Где принимается решение? | Вход из public-api в handler | На каком слое проверка обязательна и кто её владелец |
| Abuse | Какое действие нужно остановить? | Запись чужого изменения | Какие вход и состояние воспроизводят сценарий |
| Evidence | Что покажет результат? | 403 до записи, состояние не изменилось | Точный тест, код ответа, событие и состояние после запроса |
Таблица не требует выбрать STRIDE, PASTA или другой метод. OWASP прямо указывает, что его проект не задаёт единственную методику: подход выбирают по контексту, целям приватности и безопасности, зрелости команды и ограничениям поставки. Поэтому в статье фиксируется малый контракт потока, а не объявляется универсальный стандарт.
\nГраница доверия — не обязательно сетевой экран. Это место, где меняются предположения о входе или появляется новое право на действие. В нашем потоке public-api получает внешние данные, а handler решает, можно ли менять актив. Если gateway уже проверяет токен, это нужно записать отдельно: gateway подтверждает одно свойство, handler может отвечать за другое.
\nНарисуйте границу вместе с владельцем. Для каждого контроля ответьте: какой слой его выполняет, какие данные получает и что происходит при отказе. Если два слоя «проверяют авторизацию», но используют разные идентификаторы пользователя, это не избыточная безопасность, а возможное расхождение контракта. Если ни один слой не отвечает за право изменить заявку, подпись запроса проблему не закрывает.
\nНа схеме есть и обратная стрелка. Она относится только к учебному snapshot. В настоящем сервисе откат может затрагивать миграции, права, ключи, очереди и уже записанные изменения. Для них нужен отдельный план совместимости и восстановления, а не обещание «вернуть всё назад».
\n«Используем подпись» описывает механизм, но не критерий. Проверяемая формулировка звучит так: «для запроса на изменение заявки handler проверяет подлинность отправителя и его право на заявку до записи; при нарушении условия возвращает отказ, а актив остаётся неизменным». В этом предложении есть действие, точка решения, отрицательная ветка и наблюдаемый результат.
\nКакая именно технология реализует проверку, зависит от архитектуры. Для браузерного запроса могут понадобиться управление сессией, защита от CSRF (межсайтовой подделки запроса) и авторизация операции. Для webhook от сервиса-партнёра — проверка подписи тела, ограничения времени и защита от повторной доставки. Для внутреннего вызова — отдельная идентичность сервиса и политика доступа. Нельзя выбрать один из этих вариантов только по слову «API».
\nNIST SSDF задаёт высокоуровневые практики безопасной разработки, которые встраиваются в жизненный цикл, но сама публикация не сертифицирует endpoint и не заменяет проверку конкретного кода. Это полезная граница утверждения: модель угроз помогает получить требование и тест, а не выдаёт автоматический знак безопасности.
\nДо подключения сети можно проверить полноту самой записи. Следующий запуск создаёт объект в памяти, отбрасывает пустой актив, неизвестный контроль и возвращает evidence только для принятой модели. Он воспроизводим на Node.js 18 и новее, не обращается к сети и не проверяет криптографическую подпись. Скопируйте блок в терминал целиком:
\nnode --input-type=module <<'NODE'\nconst controls = new Set(['signed-request', 'session-and-authorization', 'service-identity']);\n\nfunction checkModel(input) {\n const required = ['actor', 'asset', 'boundary', 'abuse', 'control'];\n const missing = required.filter((field) => !input[field]);\n if (missing.length) return { accepted: false, reason: "missing: " + missing.join(", ") };\n if (!controls.has(input.control)) return { accepted: false, reason: 'unknown control' };\n\n return {\n accepted: true,\n claim: "reject " + input.abuse + " before writing " + input.asset,\n evidence: ["negative test at " + input.boundary + ": refusal before write"],\n rollback: { asset: input.asset, state: 'unchanged snapshot' }\n };\n}\n\nconst cases = [\n { actor: 'browser-client', asset: 'change-request:42', boundary: 'public-api->handler',\n abuse: 'unsigned request changes a request', control: 'signed-request' },\n { actor: 'browser-client', asset: '', boundary: 'public-api->handler',\n abuse: 'unsigned request changes a request', control: 'signed-request' },\n { actor: 'browser-client', asset: 'change-request:42', boundary: 'public-api->handler',\n abuse: 'unsigned request changes a request', control: 'encryption' }\n];\n\nconst results = cases.map(checkModel);\nif (!results[0].accepted || results[0].evidence.length !== 1) throw new Error('valid case failed');\nif (results[1].accepted || results[2].accepted) throw new Error('invalid case accepted');\nconsole.log(results);\nNODE\nОжидаемый результат — один принятый объект и два отказа с причинами missing: asset и unknown control. Этот результат подтверждает только структуру рассуждения. Он не подтверждает, что endpoint проверяет подпись, что идентификатор пользователя нельзя подменить или что хранилище не изменится при реальном запросе.
После фикстуры переходите к приложению. Подготовьте тестовую запись, для которой можно безопасно сравнить состояние до и после. Укажите в тесте идентификатор субъекта, заявки и права; не берите production-секреты и реальные персональные данные. Сначала отправьте допустимый запрос, затем тот же запрос с удалённым подтверждением или чужим идентификатором. Нужны не только ответ и лог, но и проверка, что запись не изменилась.
\nBASE_URL='http://127.0.0.1:3000'\nREQUEST_ID=42\n\ncurl -i --fail-with-body -X POST \\\n "$BASE_URL/v1/change-requests/$REQUEST_ID" \\\n -H 'content-type: application/json' \\\n -H 'x-test-actor: user-42' \\\n -d '{"status":"approved"}'\n\n# Отрицательный сценарий: тестовый стенд должен вернуть 401/403,\n# а GET после него — прежнюю версию заявки.\ncurl -i -X POST \\\n "$BASE_URL/v1/change-requests/$REQUEST_ID" \\\n -H 'content-type: application/json' \\\n -H 'x-test-actor: user-without-right' \\\n -d '{"status":"approved"}'\nАдрес 127.0.0.1:3000 — заменяемый адрес локального стенда, а заголовок x-test-actor — условный интерфейс тестового приложения, не стандарт безопасности. Если сервис использует cookie, OAuth или mTLS, тест должен применять его настоящий механизм в изолированной среде. Код ответа 401 или 403 выбирается контрактом API; нельзя считать любой отказ доказательством, пока состояние и аудит операции не проверены.
У теста должны быть явные precondition, действие и postcondition. До запроса: заявка принадлежит пользователю A, версия равна 7, actor не имеет права на заявку B. Действие: actor отправляет запрос изменения B. После: сервис возвращает отказ, версия и поля B не меняются, событие отказа содержит безопасный идентификатор операции. Такой набор проверяет поведение, а не наличие строчки с названием middleware.
\nЛог тоже не равен доказательству. Запись «authorization failed» полезна, если по ней можно найти запрос, слой и причину, но без раскрытия токена или персональных данных. Трассировка и метрики помогают увидеть путь, однако их отсутствие в учебной фикстуре ожидаемо. Называйте конкретный артефакт: тест, ответ, снимок состояния, событие или ручной шаг.
\nЗаранее зафиксируйте границы: тестовый стенд не показывает поведение всех прокси; один отрицательный actor не покрывает матрицу ролей; отсутствие записи в локальном журнале не доказывает отсутствие записи в очереди; проверка подписи не подтверждает защиту от повторной отправки. Эти ограничения превращают «PASS» в честный результат с понятным следующим тестом.
\nНе выбирайте механизм, если неизвестен актив или его владелец. Без этого нельзя оценить ущерб и понять, что именно должно остаться неизменным. Остановитесь, если граница спорна: сначала уточните поток и ответственность, иначе два слоя могут проверять разные свойства или не проверять ни одно.
\nНе объявляйте контроль готовым по одному успешному запросу. Он показывает доступный путь, но не показывает отказ злоупотребления. Не называйте учебную функцию security review, pentest, аттестацией или compliance evidence: она не видит реальные роли, конфигурацию, ротацию ключей, повторную доставку, лимиты, очереди и операционные журналы.
\nЕсли домен требует отдельной модели приватности, доступности или регуляторных обязательств, расширьте набор активов и участников. Если меняется архитектура, формат данных или внешняя интеграция, пересмотрите границы и угрозы. Малый поток — хороший первый срез, но не лицензия игнорировать соседние пути.
\nОдин поток можно передать на следующий этап, когда независимый проверяющий по записи называет актив, актора, границу и злоупотребление; находит контроль в конкретном месте; запускает отрицательный сценарий; видит ожидаемый отказ до изменения актива; находит артефакт проверки и понимает условия отката. Если хотя бы один пункт требует устного пояснения автора, запись ещё не завершена.
\nКритерий не означает, что угроз больше нет. Он означает, что команда понимает выбранный риск и знает, какое утверждение подтверждено. Корректный исход может быть «контроль не выбран», «проверка не выполнена» или «нужен владелец». Такой результат полезнее фиктивного PASS, не связанного с поведением приложения.
\n