Files
progcode/editorial/agent-rewrites/013.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
18 KiB
JSON
Raw 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": 13,
"slug": "editorial-2027-08-field-security-capstone",
"title": "Авторизация, которую можно доказать: от симптома до отрицательного теста",
"excerpt": "Доступ к своему профилю разрешён, к чужому — тоже. Разбираем, как связать security-требование, объектную политику, HTTP-проверку и доказательство отказа.",
"contentHtml": "<p>Пользователь открывает свой профиль, а затем меняет один идентификатор в URL и получает профиль другого пользователя. В журналах виден успешный ответ 200. В интерфейсе нет кнопки для чужого объекта, поэтому ручная проверка проходит. Цена ошибки — горизонтальная эскалация привилегий: один аккаунт читает или меняет данные другого. Исправление XSS в форме не закрывает этот путь. Экран может быть безопасным, а endpoint — нет.</p>\n<p>Тезис статьи простой: security-проверка готова только тогда, когда требование связано с конкретным субъектом, объектом и действием, а разрешение и отказ подтверждены на границе HTTP. Строка «авторизация проверена» ничего не доказывает. Доказательство содержит вход, ожидаемый ответ, фактический ответ и понятную причину расхождения.</p>\n<h2>Почему happy path вводит в заблуждение</h2>\n<p>Проверка владельца обычно начинается с положительного сценария. Пользователь u-1 запрашивает profile u-1 и получает 200. Этот тест показывает, что легитимный запрос работает. Он не показывает, что policy остановит profile u-2. Без отрицательной строки система может разрешать оба запроса.</p>\n<p>Причина часто появляется на стыке слоёв. Handler получает <code>id</code> из URL. Middleware проверяет, что токен действителен. Репозиторий ищет запись по одному <code>id</code>. Если проверка владельца не входит в запрос или выполняется после чтения, соседний объект уже попал в ответ. Кэш способен усилить ошибку: ответ для u-1 сохранится под ключом <code>profile:42</code> и станет доступен u-2.</p>\n<p>Нужна объектная проверка. Субъект приходит из проверенного контекста сессии или токена. Объект приходит из маршрута и базы. Действие задаёт endpoint. Решение принимает код рядом с границей доступа, а не только компонент интерфейса.</p>\n<h2>Требование превращается в матрицу</h2>\n<p>Фраза «пользователь видит только свой профиль» слишком короткая для теста. Разложите её на четыре поля: кто действует, что делает, над каким объектом и какой результат допустим. Для профиля минимальная политика выглядит так: user может read свой profile; user не может read чужой profile; неизвестная роль получает deny; admin может read audit, если это входит в контракт.</p>\n<div class='table-scroll'><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>user u-1</td><td>read</td><td>profile u-1</td><td>allow, 200</td></tr><tr><td>user u-1</td><td>read</td><td>profile u-2</td><td>deny, 403 или 404</td></tr><tr><td>user u-1</td><td>read</td><td>audit</td><td>deny, 403 или 404</td></tr><tr><td>unknown</td><td>read</td><td>profile u-1</td><td>deny, 401 или 403</td></tr></tbody></table></div>\n<p>Статус зависит от контракта. 403 сообщает, что запрос распознан, но запрещён. 404 иногда скрывает существование чужого объекта. Важно не выбрать «правильный» код вообще, а закрепить один вариант для конкретного endpoint и проверить его на внешней границе.</p>\n<p>Идентификатор требования должен быть стабильным. Например, <code>AUTH-PROFILE-01</code> связывает описание политики, тесты и запись изменения. Если требования ссылаются на OWASP ASVS, фиксируйте версию стандарта в ссылке. Идентификатор без версии может начать означать другой текст после обновления стандарта.</p>\n<h2>Механизм: deny по умолчанию и проверка владения</h2>\n<p>Политика должна сначала отказывать, а затем явно разрешать узкие комбинации. Для профиля нельзя проверять только роль. Два пользователя имеют одну роль, но разные объекты. Нельзя проверять только наличие токена. Валидная сессия не даёт доступ ко всем ресурсам.</p>\n<pre><code>function decide({ role, subjectId, action, resource }) {\n if (!role || !subjectId || !resource) {\n return { status: 'deny', reason: 'invalid-input' };\n }\n\n if (role === 'admin' &amp;&amp; action === 'read' &amp;&amp; resource.kind === 'audit') {\n return { status: 'allow', reason: 'role-permission' };\n }\n\n if (role === 'user' &amp;&amp; action === 'read' &amp;&amp;\n resource.kind === 'profile' &amp;&amp; resource.ownerId === subjectId) {\n return { status: 'allow', reason: 'object-ownership' };\n }\n\n return { status: 'deny', reason: 'default-deny' };\n}</code></pre>\n<p>Код выше — учебный пример. Он не является готовым middleware и не проверяет токен, CSRF, tenant, срок сессии, rate limit, кэш или журналирование. Его задача — показать форму решения: функция принимает субъект, действие и объект; результат содержит статус и безопасную причину. Не передавайте в клиент внутренние сведения о policy. Причину используйте внутри теста и журнала с учётом правил о чувствительных данных.</p>\n<p>В реальном handler сначала извлеките субъект из уже проверенного контекста, затем загрузите объект с учётом tenant и вызовите policy. Не принимайте <code>ownerId</code> из тела запроса как доказательство владения. Клиент может изменить это поле. Источник владельца — доверенная запись на сервере.</p>\n<figure><img src='/assets/editorial/2027/security-capstone-2027-review-handoff-loop.svg' alt='Цикл проверки: требование, вход, отрицательный тест, результат и разбор расхождения.' loading='lazy' /><figcaption>Проверка возвращает строку матрицы в разбор, если фактический результат отличается от ожидаемого. Отрицательные случаи остаются частью доказательства.</figcaption></figure>\n<h2>Тестируем отказ как ожидаемый результат</h2>\n<p>Отказ не должен считаться исключением теста. Он должен быть ожидаемым результатом конкретного входа. Для каждой строки зафиксируйте имя, вход и результат. Так падение отвечает на вопрос: policy разрешила чужой объект, middleware потерял subject или handler вернул неверный статус.</p>\n<pre><code>import assert from 'node:assert/strict';\n\nconst cases = [\n ['owner reads own profile',\n { role: 'user', subjectId: 'u-1', action: 'read',\n resource: { kind: 'profile', ownerId: 'u-1' } },\n { status: 'allow', reason: 'object-ownership' }],\n ['owner cannot read foreign profile',\n { role: 'user', subjectId: 'u-1', action: 'read',\n resource: { kind: 'profile', ownerId: 'u-2' } },\n { status: 'deny', reason: 'default-deny' }],\n ['unknown role is denied',\n { role: 'guest', subjectId: 'u-1', action: 'read',\n resource: { kind: 'profile', ownerId: 'u-1' } },\n { status: 'deny', reason: 'default-deny' }],\n];\n\nfor (const [name, input, expected] of cases) {\n assert.deepEqual(decide(input), expected, name);\n console.log('PASS', name);\n}</code></pre>\n<p>Этот фрагмент также учебный. Его можно выполнить только после добавления функции <code>decide</code> из предыдущего фрагмента. Он не обращается к базе и не доказывает безопасность HTTP-слоя. Если такой unit-тест зелёный, это доказывает только выбранные ветки функции. Следующий тест должен вызвать реальный handler с двумя субъектами и двумя объектами.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class='table-scroll'><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>Чужой профиль отвечает 200</td><td>Проверили токен, но не владельца объекта</td><td>Повторить запрос с u-1 к profile u-2</td><td>Добавить object-level deny в handler или policy</td></tr><tr><td>User читает audit</td><td>Роль проверяют по наличию, а не по разрешению</td><td>Запросить ресурс с user и admin</td><td>Сделать список разрешённых role/action/resource явным</td></tr><tr><td>Неизвестная роль получает доступ</td><td>Ветка по умолчанию разрешает запрос</td><td>Подать роль guest или пустую роль</td><td>Вернуть deny до всех allow-веток</td></tr><tr><td>После исправления UI тест зелёный, API уязвим</td><td>Проверяли только скрытую кнопку</td><td>Вызвать endpoint напрямую без браузерного интерфейса</td><td>Перенести контроль на серверную границу и добавить HTTP-тест</td></tr><tr><td>Иногда виден чужой ответ</td><td>Ключ кэша не содержит tenant или subject</td><td>Повторить запрос после прогрева кэша разными пользователями</td><td>Разделить ключи или запретить кэширование приватного ответа</td></tr></tbody></table></div>\n<h2>Порядок проверки</h2>\n<ol><li>Выберите один endpoint, который возвращает или меняет объект по идентификатору.</li><li>Запишите субъект, действие, объект и внешний результат в строке требования.</li><li>Найдите источник каждого поля. Subject должен прийти из доверенного контекста, owner — из серверной записи, action — из маршрута.</li><li>Добавьте один разрешённый случай и минимум три отказа: чужой объект, неподходящая роль и неизвестный объект или действие.</li><li>Проверьте policy отдельно, затем вызовите handler напрямую без UI.</li><li>Проверьте кэш, tenant-границу, сериализацию ошибки и отсутствие утечки существования объекта.</li><li>Сохраните фактические статус, тело ответа и имя проверки. При расхождении исправьте policy или контракт, а не удаляйте отрицательную строку.</li></ol>\n<h2>Ограничения и отрицательный путь</h2>\n<p>Матрица не заменяет модель угроз. Она не проверяет CSRF для изменяющего запроса, подделку токена, SSRF, загрузку файлов, гонки, обход маршрутизации и настройку reverse proxy. Для multi-tenant системы добавьте tenant в субъект и объект. Для массовых операций проверьте каждый объект, а не только первый. Для файлов отдельная проверка должна учитывать тип содержимого, имя, хранение вне webroot и выдачу через авторизованный обработчик.</p>\n<p>Не считайте зелёный unit-тест доказательством всей защиты. Отрицательный путь может сломаться между слоями: policy вернула deny, но adapter превратил его в 200; handler вернул 403, но кэш отдал старый 200; база загрузила запись другого tenant до проверки. Поэтому критерий должен проходить через реальный маршрут.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Для выбранного endpoint есть версия требования, матрица входов и автоматическая проверка. Запрос владельца получает закреплённый allow-ответ. Запрос к чужому объекту, неизвестная роль и недопустимое действие получают закреплённый deny-ответ. Прямой HTTP-вызов подтверждает это без UI. Повторная проверка после прогрева кэша не меняет результат между субъектами. В логе остаётся безопасный идентификатор проверки, но не секрет и не лишние персональные данные.</p>\n<p>Если хотя бы одна строка не имеет фактического результата, проверка не закончена. Если результат зависит от случайного состояния, сначала стабилизируйте окружение и зафиксируйте границу. Готовность — это не отсутствие замечаний в чек-листе. Это воспроизводимый отказ на запрещённом входе и воспроизводимое разрешение на допустимом.</p>\n<h2>Проверяемые источники</h2><ul><li><a href='https://owasp.org/www-project-application-security-verification-standard/' target='_blank' rel='noopener noreferrer'>OWASP Application Security Verification Standard</a> — актуальная стабильная версия 5.0.0. Используйте как каталог проверяемых требований и фиксируйте версию идентификатора.</li><li><a href='https://csrc.nist.gov/pubs/sp/800/218/final' target='_blank' rel='noopener noreferrer'>NIST SP 800-218, Secure Software Development Framework 1.1</a> — рамка практик безопасной разработки. Она не задаёт policy конкретного endpoint и не заменяет тесты.</li><li><a href='https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html' target='_blank' rel='noopener noreferrer'>OWASP File Upload Cheat Sheet</a> — рекомендации для отдельной границы загрузки и выдачи файлов. Они не доказывают безопасность авторизации профиля.</li></ul>"
}