DarkRiDDeR14 мин

Аутентификация не даёт доступ: строим deny-by-default для объекта

БезопасностьАрхитектура

Пользователь успешно вошёл в систему, но изменил id в URL и увидел чужой профиль. Цена ошибки — считать факт аутентификации разрешением на любой объект. Проверка «токен валиден» отвечает только на вопрос, кто пришёл; она не отвечает, что этому субъекту можно сделать с выбранной записью.

Построим решение из четырёх полей: субъект, роль, действие и объект. Сначала политика запрещает неизвестное, затем явно разрешает владельцу чтение профиля и администратору чтение аудита. Учебный HTTP-сервер показывает ответ 403 для чужого объекта без обращения к базе данных.

Разделяем четыре вопроса

Аутентификация устанавливает субъект. Ауторизация проверяет действие над объектом. Роль — только один вход политики; владельца и принадлежность ресурса нужно получить из доверенного слоя данных. Нельзя брать ownerId из тела запроса и затем использовать его как доказательство владения.

Минимальная модель решения
ПолеИсточникПроверка
subjectIdпроверенный токенидентификатор не меняется из body
roleclaims/политикане принимать роль из URL
actionмаршрут и методявное множество действий
resourceсерверная загрузкаобъект найден по ID
ownerIdдоменная записьсравнить с subjectId
Матрица авторизации: субъект, роль, действие, объект и владелец сходятся в решении allow или deny.
Решение строится на серверных полях. Изменение идентификатора в запросе не меняет владельца записи.

Deny-by-default сохраняет неизвестное неизвестным

Политика должна возвращать deny, если роль не распознана, действие не входит в список или объект не загружен. Это не означает, что любая ошибка должна раскрываться клиенту одинаковым текстом. Внешний ответ может быть 404 для сокрытия существования объекта, а внутреннее событие сохраняет безопасный класс причины.

Allowlist действий легче проверить, чем набор исключений. Для каждого разрешения нужны субъект, действие и условие объекта. Если правило не записано, оно не должно появляться из ветки else, которая «на всякий случай» пропускает запрос.

Учебный endpoint с проверкой владельца

Вход примера — заголовок роли и путь с идентификатором профиля. Два профиля заранее заданы в памяти процесса. Ожидаемый результат: пользователь u-1 получает 200 для своего профиля и 403 для u-2; администратор читает аудит. Это runnable локальный HTTP-обмен, но не хранилище пользователей и не готовая схема токенов.

import { createServer } from 'node:http';

const profiles = new Map([['u-1', { owner: 'u-1' }], ['u-2', { owner: 'u-2' }]]);

const server = createServer((request, response) => {
  const subject = request.headers['x-subject'];
  const role = request.headers['x-role'];
  const id = new URL(request.url, 'http://local').searchParams.get('id');
  const profile = profiles.get(id);
  const resource = id === 'audit' ? { kind: 'audit' } : profile && { kind: 'profile', owner: profile.owner };
  const allowed = (role === 'admin' && resource?.kind === 'audit') || (role === 'user' && resource?.kind === 'profile' && resource.owner === subject);
  response.writeHead(allowed ? 200 : 403, { 'content-type': 'application/json' });
  response.end(JSON.stringify({ allowed }));
});

server.listen(0, '127.0.0.1', async () => {
  const address = server.address();
  const port = address.port;
  const headers = { 'x-subject': 'u-1', 'x-role': 'user' };
  console.log((await fetch('http://127.0.0.1:' + port + '/profile?id=u-1', { headers })).status);
  console.log((await fetch('http://127.0.0.1:' + port + '/profile?id=u-2', { headers })).status);
  server.close();
});

Вывод — 200, затем 403. Входной id выбирает объект, но не его владельца: owner берётся из Map. В настоящем приложении эту запись возвращает репозиторий с проверкой tenant boundary. Если сначала выбрать объект без ограничения tenant, последующая проверка роли уже слишком поздняя.

Роль не должна скрывать объектное правило

Администратор часто имеет более широкое разрешение, но это тоже должно быть явной строкой политики: ресурс, действие и область. Нельзя сделать «admin всегда всё». Для аудита, персональных данных и действий изменения нужны отдельные права и логирование решения. Чем шире роль, тем дороже ошибка в middleware.

Веб-форма и API должны применять одну и ту же политику. Скрыть кнопку недостаточно: запрос можно отправить вручную. Проверка в handler или policy layer обязательна, а UI только улучшает понятность. Результат проверки не нужно доверять клиенту — клиент получает только ответ.

Матрица отказов

Проверяемые комбинации
СубъектОбъектДействиеРешение
user u-1profile u-1readallow
user u-1profile u-2readdeny
user u-1auditreaddeny
admin a-1auditreadallow
unknownprofile u-1readdeny

Порядок построения политики

  1. Назвать субъект, объект и действие для каждого защищаемого endpoint.
  2. Определить доверенный источник subjectId, role и ownerId.
  3. Сделать deny результатом по умолчанию для неизвестной комбинации.
  4. Добавить allow-правила по одному и проверить чужой объект, неизвестную роль и запрещённое действие.
  5. Разделить внешний код ответа и внутреннюю безопасную причину отказа.
  6. Проверить handler напрямую, без UI, чтобы запрос нельзя было обойти скрытой кнопкой.

Ограничения и следующий шаг

Пример использует заголовки вместо реального механизма аутентификации и Map вместо базы. Он показывает объектную проверку, но не решает CSRF, срок токена, tenant isolation или кэширование ответа. В проекте необходимо проверить границу каждого промежуточного слоя и не кэшировать чужой ответ под общим ключом.

Следующий шаг — взять один endpoint с параметром id, выписать матрицу субъекты × действия × объекты и добавить тест на соседнего пользователя. Если правило нельзя выразить в таблице, его будет трудно проверить и сопровождать.

Проверяемые источники

  • NIST SP 800-218 — Secure Software Development Framework (SSDF) Version 1.1 — February 2022. Даёт общий словарь практик безопасной разработки и связывает защиту с жизненным циклом продукта. Граница применимости: Рамка не подтверждает наличие контроля, уязвимости или результата в конкретном приложении.
  • OWASP Application Security Verification Standard — Version 5.0.0, released 30 May 2025. Даёт версионируемые требования для проверки технических контролей веб-приложения. Граница применимости: Стандарт не заменяет модель угроз, настройку инфраструктуры и тестирование конкретных данных.
  • NIST SP 800-53 Rev. 5 — Security and Privacy Controls — September 2020, updates as of 10 December 2020. Помогает различить сам контроль и уверенность в его работе. Граница применимости: Каталог не выбирает права вашего ресурса и не является отчётом о соответствии.