diff --git a/editorial/agent-rewrites/180.json b/editorial/agent-rewrites/180.json index 41d1e89..7670091 100644 --- a/editorial/agent-rewrites/180.json +++ b/editorial/agent-rewrites/180.json @@ -1,7 +1,7 @@ { "index": 180, "slug": "editorial-2023-01-practice-threat-model", - "title": "Модель угроз до чек-листа: назвать актив, поток и границу", - "excerpt": "Практический способ разобрать один поток до выбора контроля: назвать актив, злоупотребление, границу доверия и проверяемое действие.", - "contentHtml": "
Команда получает задачу: добавить подпись, шифрование или ограничение частоты. Через день в изменении уже есть middleware, но никто не может ответить на два вопроса: какой актив защищает контроль и на какой границе он срабатывает. В продакшене это дорого. Лишняя проверка ломает легитимный поток. Слабая проверка пропускает изменение данных. При инциденте команда не может связать отказ, запись в журнале и исходную угрозу.
\nТезис статьи простой: модель угроз начинается не с перечня контролей. Сначала нужно назвать один поток, актив, злоупотребление и границу доверия. Потом выбрать контроль и сразу описать наблюдаемое доказательство его работы. Такой порядок не доказывает безопасность системы. Он делает решение проверяемым и показывает отрицательный путь.
\nРассмотрим учебный поток. browser-client отправляет change-request через границу public-api-to-handler. Обработчик записывает запрос в хранилище. Предполагаемое злоупотребление — отправить запрос без корректной подписи. Это не карта реальной сети и не результат аудита. Пример нужен, чтобы проверить способ рассуждения на одном объекте.
В потоке есть пять разных понятий. Актив — объект, потеря или изменение которого имеет цену. Актор — источник действия. Граница доверия — место, где нельзя переносить прежнее предположение о входе. Злоупотребление — конкретное нежелательное действие. Контроль — правило, которое должно изменить это действие или его последствия.
\n| Поле | Учебное значение | Проверочный вопрос | Чего запись не доказывает |
|---|---|---|---|
| Актор | browser-client | Кто формирует вход? | Личность пользователя |
| Актив | change-request | Что нельзя потерять или исказить? | Классификацию всех данных |
| Граница | public-api-to-handler | Где вход перестаёт быть доверенным? | Полную топологию сети |
| Злоупотребление | Запрос без подписи | Какое действие нужно остановить? | Полный список атак |
| Контроль | Проверка подписи | Какое решение меняет поток? | Достаточность реализации |
| Доказательство | Отказ неподписанного запроса | Что можно наблюдать? | Результат проверки продакшена |
Граница доверия не обязана совпадать с границей сервиса. Она появляется там, где обработчик меняет отношение к данным. До границы запрос принадлежит внешнему актору. После неё приложение может использовать его для записи, очереди или вызова другого сервиса. Если обработчик принимает тело запроса как уже проверенное, злоумышленник получает возможность подменить актив ещё до проверки.
\nПоэтому контроль нужно ставить рядом с переходом, который он защищает. Проверка подписи на входе отвечает на один вопрос: можно ли принимать этот запрос как созданный доверенным источником? Она не отвечает на другие вопросы. Подпись не проверяет право пользователя изменить конкретный объект. Она не заменяет валидацию схемы, защиту от повторной отправки и журналирование решения.
\nКонтроль становится полезным, когда его связывают с условием отказа. В нашем примере условие выглядит так: запрос пересёк границу, но подпись отсутствует или не проходит проверку. Ожидаемое действие — не передавать запрос обработчику записи. Доказательство — наблюдаемый ответ с отказом и, если это предусмотрено политикой, запись причины без секретов.
\nНиже приведён учебный JavaScript-код. Он только проверяет полноту записи в памяти. Он не создаёт ключи, не проверяет криптографическую подпись и не обращается к HTTP или хранилищу. Его задача — не дать пропустить пустой актив, неизвестную границу или контроль без названного условия.
\nfunction planThreatModel(input) {\n const required = ['actor', 'asset', 'boundary', 'abuse'];\n\n for (const field of required) {\n if (typeof input?.[field] !== 'string' || !input[field].trim()) {\n return { accepted: false, reason: 'missing-' + field };\n }\n }\n\n if (input.control !== 'signed-request') {\n return { accepted: false, reason: 'unknown-control' };\n }\n\n return {\n accepted: true,\n flow: {\n source: input.actor,\n asset: input.asset,\n boundary: input.boundary,\n abuse: input.abuse\n },\n control: {\n id: input.control,\n evidence: 'unsigned request is rejected'\n }\n };\n}\n\nconst plan = planThreatModel({\n actor: 'browser-client',\n asset: 'change-request',\n boundary: 'public-api-to-handler',\n abuse: 'send request without a valid signature',\n control: 'signed-request'\n});\n\nconsole.log(plan.accepted); // true\nconsole.log(plan.control.evidence);\nПоложительный результат означает только то, что все поля заполнены и контроль известен модели. Он не означает, что подпись действительно проверяется. Для этого нужен отдельный тест обработчика с конкретным форматом подписи, ключом, временем действия и ожидаемым кодом отказа. Учебный пример намеренно не прячет эти границы за общим словом «безопасно».
\nЕсли поле asset пустое, функция должна вернуть missing-asset. Если граница не названа, результатом будет missing-boundary. Если вместо явного контроля передать строку encrypt-everything, модель должна вернуть unknown-control. Она не должна угадывать, что автор имел в виду. Молчаливое угадывание превращает неполное описание в ложное согласие.
В реальном приложении отрицательный путь проходит дальше. Неподписанный запрос должен остановиться до записи в хранилище. Ответ не должен раскрывать секрет, внутренний идентификатор ключа или причину, которая помогает перебору. Журнал должен содержать достаточно данных для расследования: время, тип решения, корреляционный идентификатор и безопасный контекст. Формат зависит от системы. Важно сначала назвать ожидаемое поведение.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| В PR написано «добавить подпись» | Контроль выбран раньше актива | Попросить назвать объект, который изменится при атаке | Записать актив и операцию отдельно |
| Все говорят о сервисе целиком | Не названа граница потока | Нарисовать источник, процесс, хранилище и переход доверия | Поставить решение на конкретный переход |
| Есть только happy path | Не описан вход, который должен быть отклонён | Передать пустую или неверную подпись | Зафиксировать код отказа и отсутствие записи |
| Тест проверяет ответ, но не состояние | Контроль отделён от последствия | Проверить, что запрос не попал в store | Добавить проверку побочного эффекта |
| В журнале написано «security check failed» | Событие нельзя связать с потоком | Найти correlation ID и безопасные поля контекста | Добавить наблюдаемое решение без секрета |
| Ссылка на стандарт заменяет решение | Каталог принят за проект системы | Спросить, какое требование применено к этому активу | Оставить ссылку как источник, а не как доказательство |
Малый DFD не перечисляет все активы и злоупотребления. Он не ранжирует риск и не выбирает владельца решения. Он не проверяет настройки прокси, identity provider, хранилища ключей, очереди и сетевые ACL. Если поток зависит от повторов, параллельных запросов или доставки через несколько доменов, одной строки о подписи недостаточно.
\nПодпись также имеет условия применимости. Нужно определить, кто подписывает запрос, как выбирают ключ, как действуют при ротации, какой срок допустим и как предотвращают повторное использование сообщения. Если система уже доверяет серверному каналу, подпись может решать другую задачу или быть лишней. Нельзя переносить учебный контроль в продакшен только потому, что он выглядит конкретно.
\nНе называйте локальную проверку security review. Не называйте отказ одного тестового запроса доказательством отсутствия уязвимостей. Считайте модель готовой только для следующего шага, а не для закрытия всей темы.
\nОдин поток можно считать подготовленным к инженерной проверке, если другой разработчик без устных пояснений находит в записи актор, актив, границу, злоупотребление, контроль и доказательство. Для отрицательного входа указаны ожидаемый отказ и отсутствие побочного эффекта. Для контроля названо хотя бы одно ограничение применимости. Для настоящей системы отдельно проверены формат входа, права, ключи и конкурентные переходы.
\nПрактический тест готовности короткий: передайте запись коллеге, который не видел обсуждение. Попросите его указать, где остановится неподписанный запрос и какое состояние останется неизменным. Если ответы расходятся, модель ещё не описывает контракт. Если ответы совпадают, можно переходить к реализации и отдельным проверкам границы.
\nКоманда получает задачу: добавить подпись, шифрование или ограничение частоты. Через день в изменении уже есть middleware, но никто не может ответить на два вопроса: какой актив защищает контроль и на какой границе он срабатывает. В продакшене это дорого. Лишняя проверка ломает легитимный поток. Слабая проверка пропускает изменение данных. При инциденте команда не может связать отказ, запись в журнале и исходную угрозу.
\nМодель угроз начинается не с перечня контролей. Сначала назовите один поток, актив, злоупотребление и границу доверия. Затем выберите контроль и опишите наблюдаемое доказательство его работы. Такой порядок не доказывает безопасность системы, но делает решение проверяемым и заранее показывает отрицательный путь.
\nРассмотрим учебный поток. browser-client отправляет change-request через границу public-api-to-handler. Обработчик записывает запрос в хранилище. Предполагаемое злоупотребление — отправить запрос без корректной подписи. Это не карта реальной сети и не результат аудита. Пример нужен, чтобы проверить способ рассуждения на одном объекте.
В потоке есть пять разных понятий. Актив — объект, потеря или изменение которого имеет цену. Актор — источник действия. Граница доверия — место, где нельзя переносить прежнее предположение о входе. Злоупотребление — конкретное нежелательное действие. Контроль — правило, которое должно изменить это действие или его последствия.
\n| Поле | Учебное значение | Проверочный вопрос | Чего запись не доказывает |
|---|---|---|---|
| Актор | browser-client | Кто формирует вход? | Личность пользователя |
| Актив | change-request | Что нельзя потерять или исказить? | Классификацию всех данных |
| Граница | public-api-to-handler | Где вход перестаёт быть доверенным? | Полную топологию сети |
| Злоупотребление | Запрос без подписи | Какое действие нужно остановить? | Полный список атак |
| Контроль | Проверка подписи | Какое решение меняет поток? | Достаточность реализации |
| Доказательство | Отказ неподписанного запроса | Что можно наблюдать? | Результат проверки продакшена |
Граница доверия не обязана совпадать с границей сервиса. Она появляется там, где обработчик меняет отношение к данным. До границы запрос принадлежит внешнему актору. После неё приложение может использовать его для записи, очереди или вызова другого сервиса. Если обработчик принимает тело запроса как уже проверенное, злоумышленник получает возможность подменить актив ещё до проверки.
\nПоэтому контроль нужно ставить рядом с переходом, который он защищает. Проверка подписи на входе отвечает на один вопрос: можно ли принимать этот запрос как созданный доверенным источником? Она не отвечает на другие вопросы. Подпись не проверяет право пользователя изменить конкретный объект. Она не заменяет валидацию схемы, защиту от повторной отправки и журналирование решения.
\nКонтроль становится полезным, когда его связывают с условием отказа. В нашем примере условие выглядит так: запрос пересёк границу, но подпись отсутствует или не проходит проверку. Ожидаемое действие — не передавать запрос обработчику записи. Доказательство — наблюдаемый ответ с отказом и, если это предусмотрено политикой, запись причины без секретов.
\nНиже приведён учебный JavaScript-код. Он только проверяет полноту записи в памяти. Он не создаёт ключи, не проверяет криптографическую подпись и не обращается к HTTP или хранилищу. Его задача — не дать пропустить пустой актив, неизвестную границу или контроль без названного условия.
\nfunction planThreatModel(input) {\n const required = ['actor', 'asset', 'boundary', 'abuse'];\n\n for (const field of required) {\n if (typeof input?.[field] !== 'string' || !input[field].trim()) {\n return { accepted: false, reason: 'missing-' + field };\n }\n }\n\n if (input.control !== 'signed-request') {\n return { accepted: false, reason: 'unknown-control' };\n }\n\n return {\n accepted: true,\n flow: {\n source: input.actor,\n asset: input.asset,\n boundary: input.boundary,\n abuse: input.abuse\n },\n control: {\n id: input.control,\n evidence: 'unsigned request is rejected'\n }\n };\n}\n\nconst plan = planThreatModel({\n actor: 'browser-client',\n asset: 'change-request',\n boundary: 'public-api-to-handler',\n abuse: 'send request without a valid signature',\n control: 'signed-request'\n});\n\nconsole.log(plan.accepted); // true\nconsole.log(plan.control.evidence); // unsigned request is rejected\n\nconst rejected = planThreatModel({\n actor: 'browser-client',\n asset: '',\n boundary: 'public-api-to-handler',\n abuse: 'send request without a valid signature',\n control: 'signed-request'\n});\n\nconsole.log(rejected.reason); // missing-asset\nПоложительный результат означает только то, что все поля заполнены и контроль известен модели; в примере выводится true. Отрицательный вызов с пустым asset выводит missing-asset. Это не означает, что подпись действительно проверяется. Для такого вывода нужен отдельный тест обработчика с форматом подписи, ключом, временем действия и ожидаемым кодом отказа. Учебный пример намеренно не прячет эти границы за общим словом «безопасно».
Если поле asset пустое, функция должна вернуть missing-asset. Если граница не названа, результатом будет missing-boundary. Если вместо явного контроля передать строку encrypt-everything, модель должна вернуть unknown-control. Она не должна угадывать, что автор имел в виду. Молчаливое угадывание превращает неполное описание в ложное согласие.
В реальном приложении отрицательный путь проходит дальше. Неподписанный запрос должен остановиться до записи в хранилище. Ответ не должен раскрывать секрет, внутренний идентификатор ключа или причину, которая помогает перебору. Журнал должен содержать достаточно данных для расследования: время, тип решения, корреляционный идентификатор и безопасный контекст. Формат зависит от системы. Важно сначала назвать ожидаемое поведение.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| В PR написано «добавить подпись» | Контроль выбран раньше актива | Попросить назвать объект, который изменится при атаке | Записать актив и операцию отдельно |
| Все говорят о сервисе целиком | Не названа граница потока | Нарисовать источник, процесс, хранилище и переход доверия | Поставить решение на конкретный переход |
| Есть только happy path | Не описан вход, который должен быть отклонён | Передать пустую или неверную подпись | Зафиксировать код отказа и отсутствие записи |
| Тест проверяет ответ, но не состояние | Контроль отделён от последствия | Проверить, что запрос не попал в store | Добавить проверку побочного эффекта |
| В журнале написано «security check failed» | Событие нельзя связать с потоком | Найти correlation ID и безопасные поля контекста | Добавить наблюдаемое решение без секрета |
| Ссылка на стандарт заменяет решение | Каталог принят за проект системы | Спросить, какое требование применено к этому активу | Оставить ссылку как источник, а не как доказательство |
Малый DFD не перечисляет все активы и злоупотребления. Он не ранжирует риск и не выбирает владельца решения. Он не проверяет настройки прокси, identity provider, хранилища ключей, очереди и сетевые ACL. Если поток зависит от повторов, параллельных запросов или доставки через несколько доменов, одной строки о подписи недостаточно.
\nПодпись также имеет условия применимости. Нужно определить, кто подписывает запрос, как выбирают ключ, как действуют при ротации, какой срок допустим и как предотвращают повторное использование сообщения. Если система уже доверяет серверному каналу, подпись может решать другую задачу или быть лишней. Нельзя переносить учебный контроль в продакшен только потому, что он выглядит конкретно.
\nНе называйте локальную проверку security review. Не называйте отказ одного тестового запроса доказательством отсутствия уязвимостей. Считайте модель готовой только для следующего шага, а не для закрытия всей темы.
\nОдин поток можно считать подготовленным к инженерной проверке, если другой разработчик без устных пояснений находит в записи актор, актив, границу, злоупотребление, контроль и доказательство. Для отрицательного входа указаны ожидаемый отказ и отсутствие побочного эффекта. Для контроля названо хотя бы одно ограничение применимости. Для настоящей системы отдельно проверены формат входа, права, ключи и конкурентные переходы.
\nЭто компактная рабочая запись, а не обязательный стандартный формат. OWASP описывает threat model через предмет моделирования, проверяемые допущения, угрозы, меры и способ валидации. NIST SP 800-154 рассматривает threat modeling как оценку риска для выбранного объекта; наши шесть полей лишь переводят эту идею в короткую карточку одного потока.
\nПрактический тест готовности короткий: передайте запись коллеге, который не видел обсуждение. Попросите его указать, где остановится неподписанный запрос и какое состояние останется неизменным. Если ответы расходятся, модель ещё не описывает контракт. Если ответы совпадают, можно переходить к реализации и отдельным проверкам границы.
\n