Files

8 lines
26 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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 &lt;-- trust boundary: public input / handler decision\n |\n v\n handler ----&gt; 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 &lt;&lt;&#039;NODE&#039;\nconst controls = new Set([&#039;signed-request&#039;, &#039;session-and-authorization&#039;, &#039;service-identity&#039;]);\n\nfunction checkModel(input) {\n const required = [&#039;actor&#039;, &#039;asset&#039;, &#039;boundary&#039;, &#039;abuse&#039;, &#039;control&#039;];\n const missing = required.filter((field) =&gt; !input[field]);\n if (missing.length) return { accepted: false, reason: &quot;missing: &quot; + missing.join(&quot;, &quot;) };\n if (!controls.has(input.control)) return { accepted: false, reason: &#039;unknown control&#039; };\n\n return {\n accepted: true,\n claim: &quot;reject &quot; + input.abuse + &quot; before writing &quot; + input.asset,\n evidence: [&quot;negative test at &quot; + input.boundary + &quot;: refusal before write&quot;],\n rollback: { asset: input.asset, state: &#039;unchanged snapshot&#039; }\n };\n}\n\nconst cases = [\n { actor: &#039;browser-client&#039;, asset: &#039;change-request:42&#039;, boundary: &#039;public-api-&gt;handler&#039;,\n abuse: &#039;unsigned request changes a request&#039;, control: &#039;signed-request&#039; },\n { actor: &#039;browser-client&#039;, asset: &#039;&#039;, boundary: &#039;public-api-&gt;handler&#039;,\n abuse: &#039;unsigned request changes a request&#039;, control: &#039;signed-request&#039; },\n { actor: &#039;browser-client&#039;, asset: &#039;change-request:42&#039;, boundary: &#039;public-api-&gt;handler&#039;,\n abuse: &#039;unsigned request changes a request&#039;, control: &#039;encryption&#039; }\n];\n\nconst results = cases.map(checkModel);\nif (!results[0].accepted || results[0].evidence.length !== 1) throw new Error(&#039;valid case failed&#039;);\nif (results[1].accepted || results[2].accepted) throw new Error(&#039;invalid case accepted&#039;);\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=&#039;http://127.0.0.1:3000&#039;\nREQUEST_ID=42\n\ncurl -i --fail-with-body -X POST \\\n &quot;$BASE_URL/v1/change-requests/$REQUEST_ID&quot; \\\n -H &#039;content-type: application/json&#039; \\\n -H &#039;x-test-actor: user-42&#039; \\\n -d &#039;{&quot;status&quot;:&quot;approved&quot;}&#039;\n\n# Отрицательный сценарий: тестовый стенд должен вернуть 401/403,\n# а GET после него — прежнюю версию заявки.\ncurl -i -X POST \\\n &quot;$BASE_URL/v1/change-requests/$REQUEST_ID&quot; \\\n -H &#039;content-type: application/json&#039; \\\n -H &#039;x-test-actor: user-without-right&#039; \\\n -d &#039;{&quot;status&quot;:&quot;approved&quot;}&#039;</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>"
}