diff --git a/editorial/agent-rewrites/179.json b/editorial/agent-rewrites/179.json index 175b49f..50c2b4d 100644 --- a/editorial/agent-rewrites/179.json +++ b/editorial/agent-rewrites/179.json @@ -1,7 +1,7 @@ { "index": 179, "slug": "editorial-2023-01-mechanism-threat-model", - "title": "Модель угроз без догадок: связать актив, риск и проверяемый контроль", - "excerpt": "Как превратить разговор о безопасности в проверяемую связь: назвать актив, границу и злоупотребление, выбрать контроль и заранее определить evidence для отрицательного пути.", - "contentHtml": "
В ревью появляется знакомый симптом: в изменении уже назван контроль — подпись, шифрование, rate limit или дополнительная проверка, — но никто не может ответить, какой актив он защищает и какой вход должен быть отвергнут. Обсуждение быстро сводится к вкусу: один инженер предлагает JWT, другой — mTLS, третий — ещё один middleware. Цена ошибки — ложное закрытие риска. Команда может потратить время на защиту не той границы, а при откате не сможет объяснить, какую гарантию она снимает.
\nТезис статьи простой: модель угроз полезна только тогда, когда связывает четыре наблюдаемых вещи — актив, злоупотребление, контроль и evidence. Название технологии не заменяет эту связь. Сначала нужно зафиксировать, что важно сохранить, какое действие нарушает это свойство, где система принимает решение и чем команда увидит отказ. Такой минимальный контракт не доказывает безопасность продукта. Он делает решение проверяемым и показывает, чего ещё не хватает.
\nВозьмём учебный поток: browser-client отправляет change-request через границу public-api-to-handler, обработчик принимает или отклоняет запрос, затем записывает его в store. Актив — не «данные вообще», а конкретный change-request. Злоупотребление — отправить запрос без valid signature. Контроль — проверять подпись до записи. Evidence — наблюдаемый отказ на неподписанном входе.
\nУ каждого поля есть один вопрос. Asset отвечает: «что потеряет свойство при ошибке?». Abuse отвечает: «какое действие мы пытаемся остановить?». Boundary отвечает: «где вход перестаёт быть доверенным?». Control отвечает: «какое решение примет система?». Evidence отвечает: «что увидит проверяющий и по какому признаку скажет pass или fail?». Если поле заменяют словами «система», «безопасность» или «проверено», модель теряет смысл.
\n| Элемент | Учебное значение | Проверяемый вопрос | Что не следует из записи |
|---|---|---|---|
| Actor | browser-client | кто формирует вход? | личность пользователя и его права |
| Asset | change-request | что нужно сохранить? | классификация всех данных продукта |
| Boundary | public-api-to-handler | где меняется доверие к входу? | полная топология сети |
| Abuse | unsigned request | какое действие не должно пройти? | полный список атак |
| Control | signed-request | какое условие проверяет handler? | достаточность криптографии |
| Evidence | local rejection check | какой отказ можно наблюдать? | результат production-проверки или pentest |
Ниже — учебный JavaScript-пример. Он не создаёт ключи, не подписывает HTTP-запросы и не обращается к базе. Функция проверяет только полноту описания потока. Это полезно для иллюстрации механизма: пропущенный актив или неизвестный контроль не превращаются в молчаливую догадку.
\nfunction planThreatModel(input) {\n const required = ['actor', 'asset', 'boundary', 'abuse', 'control', 'evidence'];\n const missing = required.filter((key) => !input[key]);\n\n if (missing.length > 0) {\n return { status: 'reject', reason: `missing: ${missing.join(', ')}` };\n }\n\n const allowedControls = ['signed-request', 'explicit-authorization'];\n if (!allowedControls.includes(input.control)) {\n return { status: 'reject', reason: 'unknown-control' };\n }\n\n return {\n status: 'accept',\n contract: {\n asset: input.asset,\n abuse: input.abuse,\n control: input.control,\n evidence: input.evidence\n }\n };\n}\n\n// Учебный вызов: это не проверка реального endpoint.\nplanThreatModel({\n actor: 'browser-client',\n asset: 'change-request',\n boundary: 'public-api-to-handler',\n abuse: 'unsigned request',\n control: 'signed-request',\n evidence: 'local rejection check'\n});\nПоложительный результат здесь означает только то, что запись содержит нужные поля и знакомый контроль. Он не означает, что подпись реализована правильно, ключи защищены, права проверены или запрос нельзя подделать другим способом. Отрицательный путь важнее happy path: пустой asset, отсутствующая boundary и неизвестный control должны остановить описание. В рабочем коде такой контракт нужно дополнить тестом самого handler, а затем отдельно проверить интеграцию и эксплуатационные границы.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Есть control, но нет asset | выбрали привычную технологию | какой объект теряет свойство? | назвать один asset до выбора механизма |
| Есть threat, но нет boundary | не указано место решения | где вход меняет статус доверия? | показать одну границу на потоке |
| Есть control, но нет evidence | не описан отрицательный путь | какой вход должен получить отказ? | добавить test, log или ручную проверку |
| В evidence написано «проверено» | не назван метод и результат | что именно увидит другой инженер? | указать вход, ожидаемый отказ и артефакт |
| Rollback означает «вернуть всё» | модель смешана с выпуском | какие поля и ресурсы реально меняются? | разделить snapshot договора и план отката |
Малая модель не ранжирует риск, не считает вероятность, не строит attack tree и не перечисляет все trust zones. Она не проверяет криптографический протокол, identity provider, права доступа, хранение секретов или журналирование. Если актив связан с персональными данными, платежами или критичной операцией, потребуется более подробная классификация, согласованный владелец риска и независимая проверка.
\nКонтроль может оказаться неверным даже при полной записи. Подпись не заменяет авторизацию. Шифрование канала не доказывает целостность бизнес-операции. Rate limit не решает проблему подмены личности. Нельзя переносить учебное signed-request на реальный API без проверки протокола, ключей, времени жизни, повторной отправки и обработки ошибок. Отрицательный результат модели — повод остановиться и уточнить решение, а не повод подобрать другое модное слово.
\nМодель готова к следующему этапу, если независимый инженер без устных пояснений может назвать asset, злоупотребление, boundary, контроль и evidence; воспроизвести отрицательную проверку; увидеть однозначный pass или fail; и понять, что именно откатывается. Это критерий полноты записи, а не сертификат безопасности. Для реализации нужен отдельный критерий: тест должен пройти на настоящем handler с разрешённым входом и явно заданным неподписанным входом, а результат должен принадлежать согласованному артефакту проверки.
\nВ ревью появляется знакомый симптом: в изменении уже назван контроль — подпись, шифрование, rate limit или дополнительная проверка, — но никто не может ответить, какой актив он защищает и какой вход должен быть отвергнут. Обсуждение быстро сводится к выбору технологии: один инженер предлагает JWT, другой — mTLS, третий — ещё один middleware. Цена ошибки — ложное закрытие риска. Команда может защитить не ту границу, а при откате не понять, какую гарантию она снимает.
\nМодель угроз помогает вернуть разговор к проверяемой цепочке. Для одного потока нужно назвать участника, актив, границу, злоупотребление, контроль и evidence — наблюдаемое доказательство результата. Название технологии не заменяет ни одного из этих полей. Ниже — учебная модель, которая проверяет полноту записи, а не доказывает безопасность продукта.
\nВозьмём учебный поток: browser-client отправляет change-request через public-api-to-handler, обработчик принимает или отклоняет запрос, затем записывает его в store. Актив — конкретный change-request, а не «данные вообще». Злоупотребление — отправить запрос без действительной подписи. Граница — место, где вход перестаёт быть доверенным. Контроль — решение handler до записи. Evidence — наблюдаемый отказ на неподписанном входе.
\nЭта схема содержит шесть полей, но отвечает на четыре вопроса. Что мы строим — actor, asset и boundary. Что может пойти не так — abuse. Что предпримем — control. Как поймём, что решение достаточно проверено, — evidence. Так модель не смешивает объект защиты, сценарий атаки и способ проверки.
\n| Поле | Учебное значение | Вопрос | Граница вывода |
|---|---|---|---|
| Actor | browser-client | кто формирует вход? | не доказывает личность и права пользователя |
| Asset | change-request | что нужно сохранить? | не заменяет классификацию всех данных |
| Boundary | public-api-to-handler | где меняется доверие к входу? | не описывает полную топологию сети |
| Abuse | unsigned request | какое действие не должно пройти? | не является полным списком атак |
| Control | signed-request | какое условие проверяет handler? | не доказывает достаточность криптографии |
| Evidence | local rejection check | что можно наблюдать? | не является результатом production-проверки или pentest |
Риск здесь — не название уязвимости и не оценка «высокий». Это связка сценария злоупотребления с последствием для актива. Если неподписанный change-request меняет запись, последствием становится потеря целостности этого актива. Контроль должен уменьшать именно такой путь, а evidence — показать, что неподписанный вход действительно не дошёл до записи.
\nКоличественную оценку риска эта заметка не вводит. Для неё потребовались бы вероятность, ущерб, стоимость исправления и согласованный способ приоритизации. Поэтому не стоит превращать слово «risk» в число без метода расчёта. Сначала зафиксируйте путь атаки и свойство актива, затем решайте, нужна ли отдельная оценка.
\nНиже — небольшой JavaScript-валидатор записи модели. Он не создаёт ключи, не проверяет подпись HTTP-запроса и не обращается к базе. Его задача уже: не дать пропустить неполное описание потока или контроль, которого нет в согласованном наборе. Запускать пример можно в Node.js или в консоли браузера.
\nfunction checkThreatRecord(input = {}) {\n const required = ['actor', 'asset', 'boundary', 'abuse', 'control', 'evidence'];\n const missing = required.filter((key) => {\n return typeof input[key] !== 'string' || input[key].trim() === '';\n });\n\n if (missing.length > 0) {\n return { status: 'reject', reason: 'missing: ' + missing.join(', ') };\n }\n\n if (input.control !== 'signed-request') {\n return { status: 'reject', reason: 'control-not-approved' };\n }\n\n return {\n status: 'accept',\n asset: input.asset,\n abuse: input.abuse,\n control: input.control,\n evidence: input.evidence\n };\n}\n\nconst complete = {\n actor: 'browser-client',\n asset: 'change-request',\n boundary: 'public-api-to-handler',\n abuse: 'unsigned request',\n control: 'signed-request',\n evidence: 'local rejection check'\n};\n\nconsole.log(checkThreatRecord(complete).status); // accept\nconsole.log(checkThreatRecord({ ...complete, asset: '' }).reason); // missing: asset\nconsole.log(checkThreatRecord({ ...complete, control: 'rate-limit' }).reason); // control-not-approved\nВызов с полной записью возвращает accept. Пустой asset и неподтверждённый control дают разные причины отказа. Это воспроизводимый результат именно для контракта описания. Он не означает, что реальный handler проверяет подпись, что ключи защищены, что запрос нельзя повторить или что пользователь имеет право изменить запись. Следующий тест должен обращаться к настоящему handler и проверять разрешённый и неподписанный входы отдельно.
Полезный поворот расследования происходит не при выборе нового middleware, а при проверке отрицательного пути. Если запись показывает контроль, но не показывает вход, который он должен отвергнуть, команда ещё не связала защиту с угрозой. Если evidence ограничено словом «проверено», другой инженер не сможет повторить вывод.
\n| Симптом | Проверка | Действие | Что ещё не доказано |
|---|---|---|---|
| Есть control, но нет asset | какой объект теряет свойство? | назвать один asset до выбора механизма | что контроль защищает весь продукт |
| Есть abuse, но нет boundary | где вход меняет статус доверия? | показать границу на потоке | что другие входы устроены так же |
| Есть control, но нет evidence | какой вход должен получить отказ? | добавить тест, лог решения или ручную проверку | что реализация уже исправлена |
| Evidence равно «проверено» | какой вход, результат и артефакт? | записать три этих значения | что тест покрывает все ветви |
| Rollback равно «вернуть всё» | какие поля и ресурсы изменились? | описать границу отката отдельно | что удаление контроля безопасно |
Минимальная модель не ранжирует риск, не считает вероятность, не строит attack tree и не перечисляет все trust zones. Она не проверяет криптографический протокол, identity provider, права доступа, хранение секретов или журналирование. Для персональных данных, платежей и критичных операций понадобятся классификация, владелец риска и независимая проверка.
\nПодпись не заменяет авторизацию. Шифрование канала не доказывает целостность бизнес-операции. Rate limit не решает проблему подмены личности. Нельзя переносить учебное signed-request на реальный API без проверки протокола, ключей, времени жизни, повторной отправки и обработки ошибок. Полная запись модели — условие для следующего этапа, а не сертификат безопасности.
\nOWASP описывает threat modeling как структурированный и повторяемый процесс: сначала моделируют систему, затем выявляют угрозы, выбирают меры и проводят review/validation. В этой рамке отдельно названы потоки данных, хранилища и trust boundaries, поэтому boundary в учебном контракте нельзя заменять общим словом «система».
\nNIST SP 800-154 связывает data-centric threat modeling с четырьмя шагами: определить систему и интересующие данные, выбрать векторы атаки, охарактеризовать меры защиты и зафиксировать результаты. Это Initial Public Draft 2016 года, а страница NIST сообщает о планах финализации; документ нельзя цитировать как завершённый стандарт. Для конкретного проекта его ограничения и актуальность нужно проверять отдельно.
\nМодель готова к следующему этапу, если независимый инженер без устных пояснений называет actor, asset, boundary, abuse, control и evidence; воспроизводит отказ на неподписанном входе; видит однозначный pass или fail; и понимает, что именно откатывается. Это критерий полноты записи. Для реализации нужен отдельный тест настоящего handler с разрешённым и явно неподписанным входом, а его результат должен принадлежать согласованному артефакту проверки.
\n