Files
progcode/editorial/agent-rewrites/014.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
16 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": 14,
"slug": "editorial-2027-08-mechanism-security-capstone",
"title": "Проверка логина не защищает объект: строим deny-by-default",
"excerpt": "Пользователь может быть правильно аутентифицирован и всё равно не иметь права читать выбранную запись. Разбираем объектную авторизацию, проверку владельца и отрицательный путь от URL до ответа 403.",
"contentHtml": "<p>Пользователь входит в систему, открывает <code>/profile?id=u-1</code>, меняет один символ и получает профиль <code>u-2</code>. Токен остаётся действительным. Сервер проверяет его наличие и отдаёт найденную запись. Симптом выглядит как обычный доступ к странице, но это горизонтальная эскалация прав.</p><p>Цена ошибки — утечка персональных данных, изменение чужих объектов и потеря границы между tenant-ами. Чем больше endpoint-ов строится по схеме «взяли id из URL, нашли запись, вернули ответ», тем больше таких точек появляется. Скрытая кнопка в интерфейсе не помогает: запрос можно повторить вручную.</p><h2>Тезис: логин устанавливает субъекта, но не разрешение</h2><p>Аутентификация отвечает на вопрос «кто отправил запрос». Авторизация отвечает на другой вопрос: «может ли этот субъект выполнить это действие над этим объектом». Между вопросами стоят роль, область доступа и принадлежность записи. Если сервер не проверяет их отдельно, валидная сессия превращается в пропуск к любому известному идентификатору.</p><p>Надёжное решение принимает четыре значения: субъект, действие, объект и контекст политики. Субъект приходит из проверенной сессии или токена. Действие выводится из маршрута и HTTP-метода. Объект загружается сервером. Владелец, tenant и чувствительность объекта берутся из доверенных данных, а не из тела запроса. Неизвестная комбинация получает deny.</p><h2>Механизм объектной проверки</h2><p>Сначала middleware или слой сессии устанавливает <code>subjectId</code> и роль. Затем handler разбирает путь и получает идентификатор ресурса. Репозиторий возвращает объект вместе с его владельцем и tenant-ом. Policy layer сравнивает эти поля с субъектом и разрешает только явно описанные действия. UI может скрыть недоступную кнопку, но решение всё равно принимает сервер.</p><p>Правило владельца нельзя заменить проверкой роли. Два пользователя могут иметь одну роль <code>user</code>, но видеть разные профили. Роль говорит о классе полномочий. Владелец говорит о конкретном объекте. Для администратора нужен отдельный allow-список: «читать аудит» не равно «читать любую персональную запись», а «администратор» не должно означать «разрешено всё».</p><p>Нельзя принять <code>ownerId</code> из JSON и использовать его как доказательство владения. Клиент сообщает, какой объект он хочет выбрать. Сервер сам читает владельца из базы или доменного сервиса. Для multi-tenant системы запрос к хранилищу должен сразу включать tenant boundary. Если сначала получить запись без ограничения области, последующая проверка уже может оказаться слишком поздней.</p><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>Замена <code>id</code> в URL показывает чужой профиль</td><td>Проверили сессию, но не владельца объекта</td><td>Отправить запрос для своего и соседнего идентификатора</td><td>Сравнивать <code>subjectId</code> с владельцем записи; чужой объект отклонять</td></tr><tr><td>Пользователь читает audit endpoint</td><td>Роль проверяют слишком широко</td><td>Вызвать endpoint с ролью <code>user</code> напрямую, без UI</td><td>Описать ресурс и действие в отдельном allow-правиле</td></tr><tr><td>Неизвестная роль получает 200</td><td>Ветка по умолчанию пропускает запрос</td><td>Удалить или подменить claim и повторить вызов</td><td>Сделать результатом по умолчанию <code>deny</code></td></tr><tr><td>Чужой ответ появляется после кэширования</td><td>Ключ кэша не содержит субъекта или области</td><td>Повторить запрос разными субъектами и сравнить тело</td><td>Разделить кэш по permission context либо не кэшировать ответ</td></tr><tr><td>403 раскрывает существование записи</td><td>Внешний ответ повторяет внутреннюю причину</td><td>Сравнить ответ для отсутствующего и чужого объекта</td><td>Выбрать 404 или 403 по модели угроз, причину логировать безопасно</td></tr></tbody></table></div><figure><img src='/assets/editorial/2027/security-capstone-2027-control-evidence-matrix.svg' alt='Матрица авторизации связывает субъекта, роль, действие, объект и владельца с решением allow или deny' loading='lazy' /><figcaption>Решение строится на серверных атрибутах объекта. Изменение идентификатора в запросе не меняет его владельца и tenant.</figcaption></figure><h2>Учебный endpoint</h2><p>Ниже — маленький пример на Node.js. Профили хранятся в <code>Map</code>, а субъект и роль передаются заголовками только для учебного сценария. Реальная система должна получать их из проверенной сессии, JWT или другого принятого механизма. Пример не подключается к production и не доказывает безопасность конкретного приложения.</p><pre><code>const profiles = new Map([['u-1', { owner: 'u-1', tenant: 't-1' }], ['u-2', { owner: 'u-2', tenant: 't-1' }]]); function decide({ role, subjectId, tenant, resource, action }) { if (role === 'admin' &amp;&amp; resource.kind === 'audit' &amp;&amp; action === 'read') return { status: 200, reason: 'admin-audit' }; if (role === 'user' &amp;&amp; resource.kind === 'profile' &amp;&amp; action === 'read' &amp;&amp; resource.tenant === tenant &amp;&amp; resource.owner === subjectId) return { status: 200, reason: 'owner' }; return { status: 403, reason: 'default-deny' }; } const id = new URL(request.url, 'http://local').searchParams.get('id'); const profile = profiles.get(id); const resource = profile &amp;&amp; { kind: 'profile', ...profile }; const result = resource ? decide({ role, subjectId, tenant, resource, action: 'read' }) : { status: 404, reason: 'not-found' };</code></pre><p>Условие для профиля проверяет роль, действие, tenant и владельца. Запрос от <code>u-1</code> к <code>u-1</code> получает 200. Тот же субъект к <code>u-2</code> получает 403. Если tenant отличается, результат также deny. Идентификатор из URL только выбирает запись; он не назначает ей владельца.</p><p>Проверка существования объекта требует отдельного решения. В примере отсутствующий профиль даёт 404, а найденный чужой — 403. В некоторых системах оба случая наружу превращают в 404, чтобы не раскрывать наличие записи. Это не универсальное правило. Выбор зависит от модели угроз. Внутри сохраняйте короткий класс причины и не пишите в журнал полный токен или секретные параметры.</p><h2>Отрицательный путь важнее happy path</h2><p>Разрешённый запрос показывает, что легитимный сценарий работает. Он не показывает, что граница закрыта. Минимальный набор должен включать чужой объект, запрещённое действие, неизвестную роль, другой tenant, отсутствующий ресурс и повторный вызов через прямой HTTP-клиент. Для mutation добавьте проверку метода и защиту от повторной операции. Для чтения проверьте кэш и сериализацию ответа.</p><p>Проверяйте policy без интерфейса. Если тест кликает только по видимой кнопке, он не проверяет handler. Отправьте запрос с изменённым <code>id</code>, вручную задайте роль и удалите обязательный claim. Эти входы учебные и не должны содержать реальные идентификаторы или секреты. Их смысл — показать отрицательную ветку, а не воспроизвести доступ к настоящим данным.</p><h2>Порядок внедрения</h2><ol><li>Составьте карту одного endpoint-а: субъект, HTTP-метод, действие, объект, tenant и внешний ответ.</li><li>Определите доверенный источник каждого поля. Не берите роль, владельца и tenant из пользовательского body.</li><li>Загрузите объект в правильной области доступа. Для tenant-системы включите tenant в запрос к хранилищу.</li><li>Запишите явные allow-правила для ресурса и действия. Оставьте <code>deny</code> результатом для неизвестной комбинации.</li><li>Добавьте тесты своего объекта, чужого объекта, запрещённого действия, неизвестной роли и другой области.</li><li>Вызовите handler напрямую, без UI, и проверьте код, тело, кэш и отсутствие лишних данных в ответе.</li><li>Настройте безопасное журналирование: причина должна помогать расследованию, но не содержать токены, пароли и полный URL с секретами.</li><li>Повторите отрицательные тесты после изменения middleware, репозитория, политики и ключа кэша.</li></ol><h2>Ограничения и критерий готовности</h2><p>Учебный код использует заголовки вместо настоящей аутентификации и <code>Map</code> вместо базы. Он не проверяет срок жизни токена, подпись JWT, CSRF, race condition, права на поля, согласованность реплик и поведение прокси. Он также не решает, как кэшировать персональный ответ. Эти вопросы требуют отдельных контрактов и тестов.</p><p>Проверка готова для одного endpoint-а, если видны источник <code>subjectId</code>, правило области, серверный способ получения владельца, явное действие и default deny. Интеграционный тест должен показать 200 для разрешённого объекта, отказ для чужого объекта и отказ для неизвестной роли через реальный HTTP-маршрут. Если проходит только unit-тест policy или только проверка UI, работа не готова: граница между запросом, хранилищем и ответом ещё не доказана.</p><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> — официальная версия стандарта с проверяемыми требованиями к контролям веб-приложения. Стандарт не выбирает роли и tenant-правила конкретного продукта.</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</a> — официальная рамка безопасной разработки. Она помогает организовать контроль, но не подтверждает, что данный endpoint уже защищён.</li><li><a href='https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final' target='_blank' rel='noopener noreferrer'>NIST SP 800-53 Rev. 5</a> — каталог security и privacy controls. Каталог не заменяет модель угроз, тесты и проверку фактических прав.</li></ul>"
}