8 lines
26 KiB
JSON
8 lines
26 KiB
JSON
{
|
||
"index": 178,
|
||
"slug": "editorial-2023-01-field-threat-model",
|
||
"title": "Модель угроз для одного потока: от симптома до проверяемого контроля",
|
||
"excerpt": "Как разобрать security-sensitive изменение, когда команда предлагает контроль, но не называет актив, границу и злоупотребление. На примере одного API-потока — схема, код, таблица диагностики и критерий готовности.",
|
||
"contentHtml": "<p>В ревью появляется знакомая фраза: «давайте добавим подпись», «закроем endpoint» или «поставим ограничение». Но никто не может ответить, какие данные защищаются и какой запрос должен быть отклонён. Команда выбирает технологию до того, как описывает угрозу. Ошибка стоит дороже лишней строки кода: контроль может сломать легитимный поток, не закрыть нужное злоупотребление и оставить владельца без доказательства результата.</p>\n<p>Разберём один поток, а не всю систему. На выходе должна получиться не красивая диаграмма, а связка: актив, актор, граница доверия, злоупотребление, контроль, проверка и откат. Пример ниже учебный: в нём нет реального сервиса, ключа или пользовательских данных. Поэтому каждый вывод будет иметь границу применимости.</p>\n<h2>Сначала зафиксируйте симптом</h2>\n<p>Начните с наблюдаемого факта. Например, обработчик принимает запрос на изменение заявки, но контракт не говорит, какое условие должно остановить неподтверждённый вход до записи. Это ещё не доказательство уязвимости: другой слой может выполнить проверку раньше. Но это достаточная причина найти владельца решения и предъявить отрицательный тест.</p>\n<p>Запишите цену ошибки двумя предложениями. Если проверка действительно отсутствует, внешний клиент может попытаться изменить чужую заявку. Если поставить слишком общий запрет, легитимные изменения начнут получать отказы, а команда создаст ручной обход. В обоих случаях спор о подписи преждевременен: сначала надо назвать объект защиты и допустимое поведение.</p>\n<p>Для учебного потока зададим следующие значения: <code>browser-client</code> отправляет запрос в <code>public-api</code>, обработчик проверяет вход и пишет <code>change-request</code> в хранилище. Актив — не абстрактные «данные», а состояние конкретной заявки и её целостность. Актор — внешний клиент, который может сформировать запрос. Злоупотребление — попытка изменить заявку без подтверждения и без права на эту операцию.</p>\n<pre><code>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: отрицательный тест показывает отказ и неизменное состояние</code></pre>\n<p>Слово «подпись» здесь не является готовым решением. Целостность сообщения, аутентификация отправителя и право изменить заявку — разные свойства. Подписанный запрос не становится автоматически разрешённым для любого пользователя; шифрование канала тоже не заменяет авторизацию. Это различие определяет, какие поля и тесты потребуются в реальном проекте.</p>\n<h2>Разложите поток на четыре вопроса</h2>\n<p>OWASP предлагает начинать с четырёх вопросов: что мы строим, что может пойти не так, что будем с этим делать и достаточно ли хорошо проверили результат. Для одного API-потока их удобно превратить в поля записи. Поля не доказывают безопасность, зато делают пропуск видимым и дают команде общий словарь.</p>\n<table><caption>Минимальная запись для одного изменения</caption><thead><tr><th scope=\"col\">Поле</th><th scope=\"col\">Вопрос</th><th scope=\"col\">Пример</th><th scope=\"col\">Что проверять</th></tr></thead><tbody><tr><td>Asset</td><td>Что нельзя потерять?</td><td>Целостность заявки 42</td><td>Какое состояние меняется и кто им владеет</td></tr><tr><td>Actor</td><td>Кто формирует вход?</td><td>Внешний клиент</td><td>Какие у него права и какие данные ему доступны</td></tr><tr><td>Boundary</td><td>Где принимается решение?</td><td>Вход из public-api в handler</td><td>На каком слое проверка обязательна и кто её владелец</td></tr><tr><td>Abuse</td><td>Какое действие нужно остановить?</td><td>Запись чужого изменения</td><td>Какие вход и состояние воспроизводят сценарий</td></tr><tr><td>Evidence</td><td>Что покажет результат?</td><td>403 до записи, состояние не изменилось</td><td>Точный тест, код ответа, событие и состояние после запроса</td></tr></tbody></table>\n<p>Таблица не требует выбрать STRIDE, PASTA или другой метод. OWASP прямо указывает, что его проект не задаёт единственную методику: подход выбирают по контексту, целям приватности и безопасности, зрелости команды и ограничениям поставки. Поэтому в статье фиксируется малый контракт потока, а не объявляется универсальный стандарт.</p>\n<h2>Поставьте границу там, где меняется доверие</h2>\n<p>Граница доверия — не обязательно сетевой экран. Это место, где меняются предположения о входе или появляется новое право на действие. В нашем потоке public-api получает внешние данные, а handler решает, можно ли менять актив. Если gateway уже проверяет токен, это нужно записать отдельно: gateway подтверждает одно свойство, handler может отвечать за другое.</p>\n<p>Нарисуйте границу вместе с владельцем. Для каждого контроля ответьте: какой слой его выполняет, какие данные получает и что происходит при отказе. Если два слоя «проверяют авторизацию», но используют разные идентификаторы пользователя, это не избыточная безопасность, а возможное расхождение контракта. Если ни один слой не отвечает за право изменить заявку, подпись запроса проблему не закрывает.</p>\n<figure><img src=\"/assets/editorial/2023/threat-model-2023-diagnosis-rollback.svg\" alt=\"Схема разбора модели угроз: от отсутствующего актива через границу и злоупотребление к контролю с доказательством и откату учебной записи\" loading=\"lazy\" /><figcaption>Последовательность вопросов для одного потока. Диаграмма показывает границы учебной записи; она не является схемой реальной сети, отчётом сканера или доказательством внедрения контроля.</figcaption></figure>\n<p>На схеме есть и обратная стрелка. Она относится только к учебному snapshot. В настоящем сервисе откат может затрагивать миграции, права, ключи, очереди и уже записанные изменения. Для них нужен отдельный план совместимости и восстановления, а не обещание «вернуть всё назад».</p>\n<h2>Сформулируйте контроль как поведение</h2>\n<p>«Используем подпись» описывает механизм, но не критерий. Проверяемая формулировка звучит так: «для запроса на изменение заявки handler проверяет подлинность отправителя и его право на заявку до записи; при нарушении условия возвращает отказ, а актив остаётся неизменным». В этом предложении есть действие, точка решения, отрицательная ветка и наблюдаемый результат.</p>\n<p>Какая именно технология реализует проверку, зависит от архитектуры. Для браузерного запроса могут понадобиться управление сессией, защита от CSRF (межсайтовой подделки запроса) и авторизация операции. Для webhook от сервиса-партнёра — проверка подписи тела, ограничения времени и защита от повторной доставки. Для внутреннего вызова — отдельная идентичность сервиса и политика доступа. Нельзя выбрать один из этих вариантов только по слову «API».</p>\n<p>NIST SSDF задаёт высокоуровневые практики безопасной разработки, которые встраиваются в жизненный цикл, но сама публикация не сертифицирует endpoint и не заменяет проверку конкретного кода. Это полезная граница утверждения: модель угроз помогает получить требование и тест, а не выдаёт автоматический знак безопасности.</p>\n<h2>Запустите минимальную фикстуру</h2>\n<p>До подключения сети можно проверить полноту самой записи. Следующий запуск создаёт объект в памяти, отбрасывает пустой актив, неизвестный контроль и возвращает evidence только для принятой модели. Он воспроизводим на Node.js 18 и новее, не обращается к сети и не проверяет криптографическую подпись. Скопируйте блок в терминал целиком:</p>\n<pre><code>node --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</code></pre>\n<p>Ожидаемый результат — один принятый объект и два отказа с причинами <code>missing: asset</code> и <code>unknown control</code>. Этот результат подтверждает только структуру рассуждения. Он не подтверждает, что endpoint проверяет подпись, что идентификатор пользователя нельзя подменить или что хранилище не изменится при реальном запросе.</p>\n<h2>Проверьте реальное поведение отдельно</h2>\n<p>После фикстуры переходите к приложению. Подготовьте тестовую запись, для которой можно безопасно сравнить состояние до и после. Укажите в тесте идентификатор субъекта, заявки и права; не берите production-секреты и реальные персональные данные. Сначала отправьте допустимый запрос, затем тот же запрос с удалённым подтверждением или чужим идентификатором. Нужны не только ответ и лог, но и проверка, что запись не изменилась.</p>\n<pre><code>BASE_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"}'</code></pre>\n<p>Адрес <code>127.0.0.1:3000</code> — заменяемый адрес локального стенда, а заголовок <code>x-test-actor</code> — условный интерфейс тестового приложения, не стандарт безопасности. Если сервис использует cookie, OAuth или mTLS, тест должен применять его настоящий механизм в изолированной среде. Код ответа 401 или 403 выбирается контрактом API; нельзя считать любой отказ доказательством, пока состояние и аудит операции не проверены.</p>\n<h2>Отделите результат от обещания</h2>\n<p>У теста должны быть явные precondition, действие и postcondition. До запроса: заявка принадлежит пользователю A, версия равна 7, actor не имеет права на заявку B. Действие: actor отправляет запрос изменения B. После: сервис возвращает отказ, версия и поля B не меняются, событие отказа содержит безопасный идентификатор операции. Такой набор проверяет поведение, а не наличие строчки с названием middleware.</p>\n<p>Лог тоже не равен доказательству. Запись «authorization failed» полезна, если по ней можно найти запрос, слой и причину, но без раскрытия токена или персональных данных. Трассировка и метрики помогают увидеть путь, однако их отсутствие в учебной фикстуре ожидаемо. Называйте конкретный артефакт: тест, ответ, снимок состояния, событие или ручной шаг.</p>\n<p>Заранее зафиксируйте границы: тестовый стенд не показывает поведение всех прокси; один отрицательный actor не покрывает матрицу ролей; отсутствие записи в локальном журнале не доказывает отсутствие записи в очереди; проверка подписи не подтверждает защиту от повторной отправки. Эти ограничения превращают «PASS» в честный результат с понятным следующим тестом.</p>\n<h2>Порядок работы</h2>\n<ol><li>Запишите симптом: какой вход, какой обработчик и какое условие отказа сейчас не видны.</li><li>Назовите один актив и защищаемое свойство: целостность, конфиденциальность, доступность или право выполнить действие.</li><li>Опишите актора и злоупотребление как действие, а не как ярлык вроде «атака».</li><li>Поставьте границу на схеме потока и назначьте владельца каждой проверки.</li><li>Сформулируйте контроль через наблюдаемое поведение до изменения актива.</li><li>Добавьте положительный и отрицательный сценарии с точным входом, кодом ответа и postcondition.</li><li>Запустите локальную фикстуру, чтобы отсеять пустые поля и неизвестные варианты контроля.</li><li>Повторите сценарий на изолированном стенде настоящего приложения и проверьте состояние, журнал или трассу.</li><li>Сохраните snapshot и опишите откат для кода, схемы данных, прав и ключей; не ограничивайтесь откатом записи модели.</li><li>Попросите независимого участника воспроизвести путь. Если он угадывает вход, владельца или критерий отказа, change не готов.</li></ol>\n<h2>Когда нужно остановиться</h2>\n<p>Не выбирайте механизм, если неизвестен актив или его владелец. Без этого нельзя оценить ущерб и понять, что именно должно остаться неизменным. Остановитесь, если граница спорна: сначала уточните поток и ответственность, иначе два слоя могут проверять разные свойства или не проверять ни одно.</p>\n<p>Не объявляйте контроль готовым по одному успешному запросу. Он показывает доступный путь, но не показывает отказ злоупотребления. Не называйте учебную функцию security review, pentest, аттестацией или compliance evidence: она не видит реальные роли, конфигурацию, ротацию ключей, повторную доставку, лимиты, очереди и операционные журналы.</p>\n<p>Если домен требует отдельной модели приватности, доступности или регуляторных обязательств, расширьте набор активов и участников. Если меняется архитектура, формат данных или внешняя интеграция, пересмотрите границы и угрозы. Малый поток — хороший первый срез, но не лицензия игнорировать соседние пути.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Один поток можно передать на следующий этап, когда независимый проверяющий по записи называет актив, актора, границу и злоупотребление; находит контроль в конкретном месте; запускает отрицательный сценарий; видит ожидаемый отказ до изменения актива; находит артефакт проверки и понимает условия отката. Если хотя бы один пункт требует устного пояснения автора, запись ещё не завершена.</p>\n<p>Критерий не означает, что угроз больше нет. Он означает, что команда понимает выбранный риск и знает, какое утверждение подтверждено. Корректный исход может быть «контроль не выбран», «проверка не выполнена» или «нужен владелец». Такой результат полезнее фиктивного PASS, не связанного с поведением приложения.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://owasp.org/www-project-threat-modeling/\" target=\"_blank\" rel=\"noopener noreferrer\">OWASP Threat Modeling Project</a> — поддерживаемое описание четырёх вопросов, границ применимости и методологически нейтрального старта; проект не объявляет одну методику обязательной.</li><li><a href=\"https://cheatsheetseries.owasp.org/cheatsheets/Threat_Modeling_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener noreferrer\">OWASP Threat Modeling Cheat Sheet</a> — практическая структура декомпозиции приложения, идентификации угроз, мер и проверки; она не заменяет анализ конкретной системы.</li><li><a href=\"https://csrc.nist.gov/pubs/sp/800/218/final\" target=\"_blank\" rel=\"noopener noreferrer\">NIST SP 800-218, SSDF 1.1</a> — финальные рекомендации NIST по безопасной разработке, опубликованные в феврале 2022 года; это высокоуровневая рамка, а не сертификат endpoint.</li><li><a href=\"https://csrc.nist.gov/pubs/sp/800/154/ipd\" target=\"_blank\" rel=\"noopener noreferrer\">NIST SP 800-154</a> — руководство по data-centric threat modeling; страница NIST помечает его как initial public draft и сообщает о планах финализации, поэтому ссылку нельзя трактовать как финальный стандарт.</li></ul>"
|
||
}
|