# Действительно ли соблюдаются границы учетных данных поддоменов API?

Имя хоста не говорит о принадлежности. Это часть идентификатора назначения, который определяет, куда отправится учетная запись API. Если клиент хранит один bearer-токен и считает `api.example.com`, `files.example.com`, `api.eu.example.com` и `customer.example.com` практически одинаковыми только потому, что у них общий суффикс, опасное решение уже принято.

Такая ошибка часто скрывается за удобной архитектурой. Компания может владеть всеми именами в родительском домене. DNS может направлять несколько имен на один балансировщик. Один сертификат может покрывать их все. Но это не означает, что учетные данные, выданные для одного API, должны попадать на другой хост. Для учетных данных нужно явное правило назначения, а для этого правила нужны тесты, которые упадут, если кто-то позже расширит его без проверки.

## Хосты не наследуют доверие от родительского домена

`api.example.com` и `admin.example.com` могут иметь общий регистрируемый домен, но это разные источники. Источник включает схему, хост и порт. `https://api.example.com` и `https://api.example.com:8443` тоже являются разными источниками. Это важно, потому что клиент выбирает сетевое назначение по этой полномочности, а не по маркетинговой связи между сервисами.

RFC 9110 определяет область защиты HTTP-аутентификации через источник и, если он указан, область realm. В документе также говорится, что одной области realm недостаточно: так можно раскрыть учетные данные другим ресурсам того же источника. Когда нужна изоляция разных сторон, рекомендуется использовать отдельные хосты или порты. Это полезное предупреждение, но оно не разрешает считать все имена под одним суффиксом единой областью защиты.

Инженеры часто смешивают три разные идеи:

- Родительский домен задает пространство имен DNS.
- Сайт объединяет связанные веб-источники для некоторых правил безопасности браузера.
- Источник определяет одну схему, хост и порт назначения HTTP.

Только первые две идеи создают впечатление, что поддомены связаны. Диспетчер API-учетных данных должен опираться на третью.

Это важно даже тогда, когда издатель токена помещает в него широкое утверждение `aud`. Широкая аудитория сообщает принимающему сервису, что он может принять. Она не указывает клиенту отправлять токен на каждую конечную точку, которая потенциально способна его принять. Отправитель по-прежнему обязан ограничивать распространение.

Я видел, как это происходит по одному и тому же сценарию. Команда начинает с одной конечной точки, добавляет `api.staging.example.com` и оставляет помощник с названием `getApiKey()`. Через полгода этот помощник используется пятью сервисами. У него нет аргумента хоста, списка разрешений и понимания, почему вызов нужно отклонить. Ключ стал небезопасным не из-за экзотической атаки. Код перестал хранить сведения о том, что ключ предназначен для одного конкретного адресата.

Сделайте привязку к хосту видимой в конфигурации и при проверке кода. Функция выбора учетных данных, которая принимает только имя ключа, уже не получает половину необходимых входных данных.

## Четыре типа имен хостов ломаются по-разному

Для соседних, vanity-, региональных и клиентских доменов нужны отдельные тестовые случаи, поскольку причины отказа у них различаются. Одно расплывчатое утверждение, что токен остается «в наших доменах», пропустит как минимум один из этих вариантов.

**Соседний хост** находится рядом с нужным: `metrics.example.com` вместо `api.example.com`. На нем может оказаться общий входной шлюз, старый сервис или внутренний инструмент, который никогда не должен видеть рабочие учетные данные. Особенно опасен такой хост, если общий прокси передает заголовки без изменений на вышестоящий сервис, выбранный настройкой маршрута.

**Vanity-хост** это удобный псевдоним вроде `api.brand.example` или `developer.example.com`. Такие имена появляются при переименовании продукта, покупке компании или миграции. Псевдоним может завершаться на другом периферийном узле, использовать другую телеметрию или перенаправлять на канонический хост. Не разрешайте ему доступ к учетным данным только потому, что однажды увидели перенаправление в браузере.

**Региональный хост** меняет расположение и часто владельца сервиса: `api.us.example.com`, `api.eu.example.com` или `api.ap-southeast.example.com`. До того как приложение отклонит токен, запрос может уже передать деловые данные. Ответ 401 не доказывает, что отправка учетных данных и тела запроса в этот регион была приемлемой.

**Клиентский хост** делает ошибки в многоклиентской системе еще серьезнее: `acme.vendor.example` и `northwind.vendor.example` могут проходить через одну инфраструктуру, но каждое имя обозначает границу клиента. Токен, достаточно широкий для работы на обоих хостах, может быть намеренно нужен центральной плоскости управления. Но он не должен по умолчанию использоваться для запросов, направленных клиентам.

Внесите каждую категорию в инвентаризацию. Не сводите их к полю `allowed_domains` и не считайте, что подстановочный знак упрощает политику. Он лишь прячет сложное решение.

## Привязывайте выбор учетных данных ко всей полномочности

Безопасный клиент сопоставляет полномочность запроса с одной записью учетных данных, а если совпадения нет, отклоняет запрос. Полная полномочность включает схему, нормализованное имя хоста и фактический порт. Для обычного HTTPS API на порту 443 видимая конфигурация может оставаться простой, но реализация все равно должна отклонять неожиданный порт, а не молча использовать тот же токен.

Рабочая инвентаризация может выглядеть так:

```yaml
credentials:
  billing-production:
    allowed:
      - https://api.billing.example.com:443
    header: Authorization
    scheme: Bearer

  telemetry-eu:
    allowed:
      - https://ingest.eu.example.net:443
    header: X-Write-Key
```

Дело не в YAML. Важно, что имя токена не существует само по себе. У `billing-production` есть одно явно указанное назначение, а `telemetry-eu` не утечет на американскую конечную точку только потому, что вызывающий код изменил строку хоста.

Избегайте такого шаблона:

```js
const headers = {
  Authorization: `Bearer ${process.env.PRODUCTION_API_TOKEN}`
};

await fetch(userSuppliedUrl, { headers });
```

Здесь неограниченное назначение связано с учетными данными высокого уровня. Разработчики иногда возражают, что URL берется из надежного файла конфигурации. Но этот файл тоже является границей входных данных. Его могут изменить инструменты развертывания, переменные окружения, запрос на слияние, функциональный флаг или взломанная задача сборки.

Используйте конструктор запросов, который не создает аутентифицированный запрос, пока не сопоставит нормализованное назначение. Нормализация хоста должна быть простой и строгой:

1. Разбирайте URL настоящим анализатором URL, а не проверкой суффикса строки.
2. Требуйте `https:`, кроме документированного исключения для локальной разработки.
3. Приводите имя хоста к нижнему регистру и каноническому виду через анализатор.
4. Сравнивайте получившиеся схему, хост и порт с точными разрешенными адресами.
5. Добавляйте заголовок аутентификации только после успешного совпадения.

Не пишите `host.endsWith("example.com")`. Так пройдет `notexample.com`. Не считайте задачу решенной, заменив это на `endsWith(".example.com")`. Такое выражение по-прежнему дает токен каждому текущему и будущему поддомену, включая имя, делегированное клиенту, поставщику или забытой среде разработки.

Не смешивайте авторизацию назначения с авторизацией учетных данных. Принимающий API может отклонить токен, который ему не предназначен. Но отправитель должен не дать токену уйти на этот API. Первый контроль ограничивает раскрытие, второй определяет доступ. Нужны оба.

## Перенаправление создает новое назначение

Обработка перенаправлений создает вторую границу для учетных данных. Первый хост может быть разрешен, а затем вернуть заголовок `Location`, указывающий на vanity-имя, региональную конечную точку, объектное хранилище или хост, который атакующий контролирует через открытое перенаправление. Если клиент следует перенаправлению автоматически, перед отправкой любых учетных данных или тела запроса он должен повторно проверить назначение.

libcurl особенно наглядно показывает разницу. По умолчанию он не отправляет внутренне созданные данные аутентификации или явно заданные заголовки cookie на другой хост во время перенаправления. Опция `CURLOPT_UNRESTRICTED_AUTH` меняет это поведение и может отправлять учетные данные на хосты, выбранные ответами перенаправления. Проект curl отдельно предупреждает, что с пользовательскими заголовками нужно быть осторожнее: библиотека не может понять, какие произвольные заголовки содержат секреты.

Именно эта деталь часто вводит опытные команды в заблуждение. Они проверяют Basic-аутентификацию стандартным клиентом и видят, что межхостовое перенаправление обрабатывается безопасно. Затем рабочая интеграция использует `X-Api-Key`, `Authorization: Bearer` или `X-Signature`, добавленные как обычный заголовок. Библиотека может сохранить такой заголовок, если приложение само его не удалит. Успешный тест одного механизма аутентификации ничего не говорит о другом.

Обрабатывайте перенаправления по типу запроса:

- Для операций записи отклоняйте перенаправления, если они не предусмотрены контрактом API.
- Для операций чтения проверяйте каждое новое назначение и заново собирайте заголовки по записи учетных данных этого адресата.
- Для подписанных запросов создавайте подпись заново после авторизации конечного назначения. Никогда не передавайте подпись, созданную для первого хоста.
- Для загрузок не передавайте исходный заголовок авторизации на URL хранилища с предварительной подписью. Ограниченное право уже передается через подпись в запросе или поля формы.

В тесте перенаправления нужно проверять запрос, который приходит на второй сервер. Одного конечного кода ответа недостаточно. Ответ 200 от безопасного тестового приемника может скрыть, что он получил рабочий заголовок, который вы собирались защитить.

Те же правила относятся к повторным попыткам. Некоторые HTTP-обертки восстанавливают запросы из кэшированной карты заголовков. Если повторная попытка следует за обнаружением сервиса к новому адресу, удалите кэшированную карту и снова обратитесь к селектору учетных данных. Повторно использовать заголовки быстрее, но именно так исчезает контекст назначения.

## Создайте стенд отрицательных тестов до того, как доверите списку разрешений

Главный тест отрицательный: учетные данные должны появиться на нужном хосте и не должны появиться на каждом правдоподобном неправильном хосте. Это можно проверить на двух локальных HTTPS-серверах, но приемник должен записывать только наличие и название чувствительных заголовков. Не помещайте настоящие значения в тестовые журналы.

Для проверки на уровне shell сопоставьте безопасные имена с локальными серверами через опцию `--resolve` в curl и используйте одноразовый токен. Запустите один приемник на порту 8443 для разрешенного хоста, а другой на 9443 для соседнего. Каждый приемник должен выводить запись такого вида:

```text
host=api.test.example
path=/v1/ping
authorization=present
x-api-key=absent
```

Приемник соседнего хоста должен выводить обратный результат:

```text
host=metrics.test.example
path=/v1/ping
authorization=absent
x-api-key=absent
```

Затем проверяйте настоящую обертку HTTP-клиента, а не ее упрощенную копию. Проба curl может показать базовую связку:

```bash
curl --silent --show-error \
  --resolve api.test.example:8443:127.0.0.1 \
  --header 'Authorization: Bearer test-token-do-not-use' \
  https://api.test.example:8443/v1/ping
```

Эта команда намеренно помещает заголовок в запрос, поэтому она показывает только то, что записывает приемник. Она не доказывает, что приложение безопасно выбирает учетные данные. Тест приложения должен вызвать обычную функцию `request()` с тем же разрешенным адресом, а затем повторить вызов для каждого неправильного адреса и проверить, что функция выдает ошибку до открытия соединения.

Используйте матрицу, которая заставляет принять решения, обычно оставляемые без внимания:

| Запрошенный адрес | Ожидаемое поведение учетных данных |
| --- | --- |
| `https://api.test.example` | Отправить предназначенные для теста учетные данные |
| `https://metrics.test.example` | Отклонить до отправки |
| `https://api.eu.test.example` | Отклонить, если адрес не настроен отдельно |
| `https://tenant-a.test.example` | Отклонить, если привязка к клиенту не задана явно |
| Разрешенный хост перенаправляет на соседний | Следовать только после повторной авторизации, обычно без исходных учетных данных |

Не используйте для этого внешний сервис сбора запросов. Иначе вы приучите команду отправлять тестовые секреты третьей стороне, пока проверяете, отправляет ли она секреты третьим сторонам. Локальный приемник легко создать, а данные останутся под вашим контролем.

Добавьте матрицу в непрерывную интеграцию. Модульный тест `isAllowedHost()` полезен, но интеграционный тест ловит типичный регресс: кто-то добавляет заголовок по умолчанию на нижнем уровне HTTP после того, как проверка хоста уже прошла.

## Правила браузера не распространяются на клиентов и агентов

Браузерная терминология порождает неверные предположения в серверном и агентском коде. «Один сайт» может включать поддомены, а «один источник» нет. MDN приводит `https://example.org` и `https://login.example.org` как пример двух источников, относящихся к одному сайту. Это различие важно, поскольку взломанный поддомен может атаковать соседний через правила одного сайта.

По умолчанию Fetch использует `credentials: "same-origin"`, поэтому браузерный fetch автоматически не добавляет учетные данные в межисточниковые запросы. Разработчик может увидеть это значение, проверить вызов из интерфейса и решить, что bearer-токен не перейдет с одного поддомена на другой. Для серверного кода это неверно. Серверная обертка fetch может добавить любой заголовок, который передал автор. CLI делает то же самое. Автономный агент может вызвать универсальный HTTP-инструмент с URL и заголовками, если инструмент не удерживает учетные данные вне его доступа.

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

Cookies создают еще один источник путаницы. Атрибуты домена cookie могут разрешить отправку на поддомены, а host-only cookie этого не делают. У bearer-заголовков нет сопоставимой встроенной области домена. Если клиент добавляет `Authorization`, он явно принимает решение для этого запроса. Не переносите модель cookies на API-ключи.

## Для клиентских доменов нужна граница издателя, а не соглашение об именах

Центральный API действительно может обращаться к множеству клиентских конечных точек, но ему нужны учетные данные, объясняющие, почему вызов может пересекать границы клиентов. Самый безопасный вариант дает каждому клиентскому хосту отдельную запись с узкой областью действия. Следующий по надежности вариант использует короткоживущий токен с аудиторией и признаками клиента, которые проверяет адресат, а список разрешений клиента по-прежнему перечисляет каждый допустимый хост.

Глобальный токен с широкими правами популярен, потому что упрощает подключение. Добавьте клиента, направьте интеграцию на его поддомен, и вызов работает. Та же простота превращает опечатку, вредоносное изменение конфигурации или ошибку агента в возможность обратиться к сервису другого клиента с учетными данными, у которых нет meaningful ограничения по назначению.

Не решайте проблему шаблоном `*.customers.example.com`, объявляя каждый совпавший адрес клиентским. Спросите, кто может создавать такие имена, кто управляет делегированием DNS, какие хосты ведут в предварительные среды и сохраняются ли имена после отключения клиента. Подстановочный знак превращает все эти вопросы в решения безопасности, обычно без записи о проверке.

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

Та же проблема возникает с частным DNS внутри компании. `payments.prod.internal` и `payments.dev.internal` могут быть недоступны из интернета, но это разные назначения с разными операционными средствами контроля. Внутренний DNS не заменяет ограничение области действия учетных данных.

## Проверяйте хост до обнаружения сервиса и после нормализации

Обнаружение сервисов, собственная маршрутизация и настройки прокси могут незаметно обойти разумный список разрешений. Если код проверяет исходный URL, а затем преобразователь заменяет адрес на внутреннюю цель, приложение может отправить учетные данные другой полномочности. И наоборот, проверка изменяемого заголовка `Host` может разрешить запрос, хотя сетевое соединение установлено в другое место.

Используйте полномочность разобранного URL как вход для политики. Устанавливайте TLS-соединение с этим адресом и обычно проверяйте сертификат. Не отключайте проверку сертификата ради внутреннего маршрута или тестового стенда. В рекомендациях по безопасности самого curl говорится: клиент, который не может аутентифицировать узел, не знает, что подключился к нужному серверу.

Затем определите, что означает прокси в вашей модели доверия. Прямой прокси это транспортный выбор, а не новое назначение учетных данных, если клиент устанавливает TLS с разрешенным источником через него. Обратный прокси, завершающий TLS, входит в границу сервиса и требует такой же проверки, как сам API. HTTP-прокси, получающий незашифрованные заголовки авторизации, имеет доступ к учетным данным. Не называйте это «просто инфраструктурой».

Нормализуйте интернационализированные имена хостов через реализацию URL, соответствующую стандартам, и сравнивайте канонический результат. Отклоняйте URL с данными пользователя вроде `https://token@api.example.com/`: учетные данные в URL попадают в журналы, историю и отладочный вывод. Отклоняйте фрагменты в HTTP-запросах и явно решайте, могут ли строки запроса содержать предварительно подписанные учетные данные. Универсальная функция очистки не спасет дизайн, который принимает любой URL и надеется потом найти плохие варианты.

Правильный порядок прост: разобрать, нормализовать, авторизовать назначение, выбрать учетные данные, создать заголовки, подключиться. Если последующая операция меняет полномочность, начните проверку назначения заново.

## Аудитируйте решение, а не секрет

Запись аудита должна показывать, какое назначение рассматривал клиент и какое правило учетных данных выбрал, не сохраняя сам секрет. Такая запись нужна, когда кто-то спрашивает, мог ли токен после изменения развертывания попасть на соседний хост.

Храните, например, идентификатор запроса, время, нормализованный адрес, идентификатор записи учетных данных, результат авторизации, исходный и конечный адреса перенаправления и код результата. Если пути содержат идентификаторы клиентов, хешируйте или скрывайте их. Не записывайте `Authorization`, пользовательские заголовки ключей, строки запроса с подписями и полные тела запросов только потому, что они удобны для отладки.

Хорошая запись аудита отвечает на конкретный вопрос:

```text
request_id=01J...
authority=https://api.billing.example.com:443
credential=billing-production
destination_check=allowed
redirect_count=0
result=201
```

Для отклоненного запроса к соседнему хосту запись должна показывать `credential=none` и `destination_check=denied`. Это подтверждает, что клиент отказал до выбора секрета. Если журнал сначала содержит имя учетных данных, а затем сообщает о 403 от неправильного хоста, система уже отправила больше, чем должна была.

Sallyport хранит секрет в зашифрованном хранилище и проверяет действие до того, как агент получает любой материал учетных данных. Журналы Activity и Sessions помогают изучить путь действия и отозвать сеанс работающего агента, если запрос с привязкой к хосту пошел не туда.

Сначала составьте инвентаризацию. Для каждой учетной записи укажите одно предназначенное для нее назначение, а затем назовите один соседний, один vanity- или миграционный, один региональный и один клиентский адрес, который не должен ее получать. Если ваш клиент сегодня не может выполнить такие отрицательные проверки, у него нет границы учетных данных. Есть только соглашение, на которое надеются.
