Доверенные прокси

Когда OxPHP работает за обратным прокси (Kubernetes Ingress, Cloudflare, AWS ALB, nginx), каждый запрос приходит с IP-адреса прокси. Без настройки доверенных прокси ограничение частоты запросов, журналирование доступа и $_SERVER['REMOTE_ADDR'] — все видят IP прокси вместо реального клиента.

Конфигурация

bash
# Comma-separated CIDR list TRUSTED_PROXIES="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" # Shorthand: all RFC-1918 + loopback + link-local (IPv4 and IPv6) TRUSTED_PROXIES="private" # Keywords and CIDRs combine in one list TRUSTED_PROXIES="private,cloudflare,100.64.0.0/10"

Каждая запись в списке через запятую — это CIDR, голый IP-адрес (отдельный хост) или одно из ключевых слов private и cloudflare, в любом регистре. Запись, не являющаяся ничем из этого, приводит к ошибке при запуске.

Note

Если переменная не задана, OxPHP игнорирует все заголовки перенаправления. Это безопасное значение по умолчанию.

Как это работает

Когда запрос приходит с доверенного IP, OxPHP проверяет заголовки перенаправления в порядке приоритета:

  1. Forwarded (RFC 7239) — стандартизированный заголовок
  2. X-Forwarded-For / X-Forwarded-Proto / X-Forwarded-Host / X-Forwarded-Port — де-факто резервный вариант

Если присутствует заголовок Forwarded, заголовки X-Forwarded-* игнорируются.

Извлечение IP-адреса клиента

OxPHP использует алгоритм rightmost-non-trusted (крайний правый недоверенный) — тот же подход, что и в nginx (real_ip_recursive on), Caddy, Traefik и Apache:

text
X-Forwarded-For: 203.0.113.50, 172.16.1.1, 10.0.0.5 TCP peer: 10.0.0.1 (trusted) Walk right-to-left: 10.0.0.5 → trusted → skip 172.16.1.1 → trusted → skip 203.0.113.50 → NOT trusted → client IP

Это предотвращает подмену через добавленные слева значения. Атакующий может добавить поддельные IP слева, но крайний правый недоверенный IP был установлен последним доверенным прокси в цепочке.

Запись может нести порт: 203.0.113.50:4711 или [2001:db8::1]:4711 — в такой форме некоторые балансировщики нагрузки пишут адрес в X-Forwarded-For (среди них Azure Application Gateway). Когда адрес клиента берётся из такой записи, её порт становится REMOTE_PORT.

Обход останавливается на записи без адреса или с адресом, который не разбирается как IP-адрес (пустой элемент, unknown, обфусцированный идентификатор RFC 7239), а также на строке заголовка, которую OxPHP не может прочитать как текст (например, содержащей не-ASCII байт). Ничто левее этой точки не используется, поскольку эта часть цепочки может оказаться лишь тем, что прислал клиент. Адрес клиента — крайняя правая недоверенная запись правее этой точки. Если все записи там доверенные, это самая левая из них; если там нет ни одной записи, адрес не перезаписывается и остаётся адресом подключившегося узла, включая порт.

text
X-Forwarded-For: 198.51.100.9, unknown, 10.0.0.5 TCP peer: 10.0.0.1 (trusted) Walk right-to-left: 10.0.0.5 → trusted → skip unknown → does not parse → stop 198.51.100.9 → never reached Client IP: 10.0.0.5 (every entry right of the stop is trusted → the leftmost of them)

С ключевым словом cloudflare запрос, цепочка перенаправления которого показывает, что он пришёл с пограничного узла Cloudflare, вместо этого берёт адрес клиента из заголовка CF-Connecting-IP. См. За CDN и балансировщиком нагрузки платформы.

Что меняется

Когда TRUSTED_PROXIES настроен и подключающийся IP является доверенным:

Компонент Без доверенных прокси С доверенными прокси
$_SERVER['REMOTE_ADDR'] IP прокси Реальный IP клиента; IP прокси, когда заголовки перенаправления не дают адреса
$_SERVER['REMOTE_PORT'] Исходный порт прокси Порт клиента из записи ip:port в Forwarded или X-Forwarded-For, иначе 0; порт узла, когда адрес не перезаписывается
$_SERVER['HTTPS'] На основе TLS-конфигурации OxPHP Из Forwarded: proto= или X-Forwarded-Proto
$_SERVER['REQUEST_SCHEME'] http или https из TLS Из перенаправленного протокола
$_SERVER['SERVER_NAME'] Из заголовка Host Из Forwarded: host= или X-Forwarded-Host
$_SERVER['SERVER_PORT'] Из заголовка Host Из X-Forwarded-Port, иначе порт из X-Forwarded-Host / Forwarded: host=, иначе 443/80
Ограничение частоты запросов По IP прокси По IP клиента
Журнал доступа IP прокси Реальный IP клиента

Когда адрес клиента перезаписывается, REMOTE_PORT — это исходный порт клиента, если запись, из которой взят адрес, его несёт (узел RFC 7239 Forwarded: for="ip:port" или запись ip:port в X-Forwarded-For), и 0 в противном случае. Порт невозможно восстановить, поэтому вместо угадывания он обнуляется. У адреса, взятого из CF-Connecting-IP, порта тоже нет, поэтому и тогда REMOTE_PORT равен 0. Когда заголовки не дают адреса клиента и адрес не перезаписывается, как описано в разделе Извлечение IP-адреса клиента, REMOTE_PORT остаётся портом подключившегося узла.

Сети private

Сокращение private включает:

Сеть Описание
10.0.0.0/8 Частная сеть класса A
172.16.0.0/12 Частная сеть класса B
192.168.0.0/16 Частная сеть класса C
127.0.0.0/8 Loopback
169.254.0.0/16 Link-local
::1/128 IPv6 loopback
fc00::/7 IPv6 unique local
fe80::/10 IPv6 link-local

Общее адресное пространство (100.64.0.0/10, RFC 6598) в private не входит. Если ваш балансировщик нагрузки подключается из него, укажите его рядом с ключевым словом: TRUSTED_PROXIES="private,100.64.0.0/10".

Сети cloudflare

Ключевое слово cloudflare добавляет пограничные диапазоны, которые Cloudflare публикует на cloudflare.com/ips: 15 диапазонов IPv4 и 7 диапазонов IPv6 по состоянию на 2026-10-05. Список вкомпилирован в OxPHP и меняется только с выпуском OxPHP. Если список Cloudflare изменится раньше:

  • Новый диапазон. Укажите его рядом с ключевым словом: TRUSTED_PROXIES="private,cloudflare,<new range>". Указанный диапазон считается одним из ваших собственных прокси, а не пограничным узлом Cloudflare, поэтому запросы через него получают адрес по правилу rightmost-non-trusted, а не из CF-Connecting-IP, пока выпуск не добавит этот диапазон в ключевое слово.
  • Отозванный диапазон. Его нельзя вычесть из ключевого слова. Чтобы исключить его раньше, чем это сделает выпуск OxPHP, перечислите диапазоны, которым вы по-прежнему доверяете, в виде CIDR вместо ключевого слова. Это также означает отказ от чтения CF-Connecting-IP, которое привязано к ключевому слову.

За CDN и балансировщиком нагрузки платформы

Распространённая схема развёртывания ставит CDN перед балансировщиком нагрузки хостинговой платформы:

text
client → Cloudflare → load balancer → OxPHP

При TRUSTED_PROXIES=private доверенным считается только балансировщик нагрузки. Крайним правым недоверенным адресом в X-Forwarded-For тогда оказывается пограничный узел Cloudflare, подключившийся к балансировщику, и каждый запрос выглядит пришедшим от Cloudflare — в REMOTE_ADDR, журнале доступа, ограничении частоты запросов и атрибуте спана client.address. Доверяйте обоим звеньям:

bash
TRUSTED_PROXIES="private,cloudflare"

Если балансировщик нагрузки подключается с адреса вне диапазонов private, укажите и его диапазон: TRUSTED_PROXIES="private,100.64.0.0/10,cloudflare".

С ключевым словом cloudflare OxPHP определяет адрес клиента в три шага:

  1. 1
    Пройти мимо собственных прокси. Начиная с подключившегося узла, OxPHP идёт влево по цепочке перенаправления (Forwarded, если он есть, иначе X-Forwarded-For) мимо ваших собственных прокси: диапазонов private и указанных вами CIDR. Адрес внутри диапазонов Cloudflare считается адресом Cloudflare, даже если вы его указали.
  2. 2
    Найти звено Cloudflare. Если первое звено, не являющееся одним из ваших собственных прокси, — адрес Cloudflare, значит, запрос пришёл через Cloudflare, и адрес клиента берётся из заголовка CF-Connecting-IP. Заголовок должен встречаться ровно один раз и содержать корректный IP-адрес.
  3. 3
    Иначе — rightmost-non-trusted. Если запрос пришёл не через Cloudflare или CF-Connecting-IP отсутствует, повторяется или некорректен, применяется правило rightmost-non-trusted, при этом диапазоны Cloudflare считаются доверенными.

Звено, на котором останавливается обход, — это подключившийся узел или адрес, записанный одним из ваших собственных прокси. Поэтому клиент, обращающийся к балансировщику нагрузки напрямую, не может заставить OxPHP прочитать CF-Connecting-IP, который он прислал сам: балансировщик записывает туда собственный адрес клиента, а до адреса Cloudflare, который клиент вписал левее, обход никогда не доходит. Как и правило rightmost-non-trusted, это опирается на то, что ваши прокси поддерживают заголовок, который читает OxPHP, — дописывают адрес подключившегося к ним узла, а не пропускают значение клиента без изменений.

Удаляйте заголовок Forwarded от клиента

Ваши прокси должны поддерживать и Forwarded. OxPHP читает его в приоритете перед X-Forwarded-For, а Cloudflare передаёт его дальше в том виде, в каком его прислал посетитель, поскольку это не один из заголовков, которые устанавливает Cloudflare. Поэтому балансировщик нагрузки, поддерживающий только X-Forwarded-For, должен удалять Forwarded из запроса. Если он пропускает этот заголовок, любой клиент — через Cloudflare или нет — выбирает адрес, который видит OxPHP, с ключевым словом cloudflare или без него.

CF-Connecting-IP не читается и тогда, когда обход останавливается, не дойдя до адреса Cloudflare, — на записи без адреса или с неразбираемым адресом либо на строке, которую OxPHP не может прочитать как текст, как описано в разделе Извлечение IP-адреса клиента. На запись, которую один из ваших прокси помещает на отдельную строку, не влияет нечитаемая строка, присланная клиентом перед ней. Запись, которую прокси пишет в ту же строку, что и текст, присланный клиентом, теряется вместе с этой строкой: обход останавливается на ней, и адрес берётся из того, что ваши прокси записали правее этой строки, или остаётся адресом подключившегося узла — но никогда не из того, что прислал клиент.

Cloudflare Workers

Cloudflare дописывает в X-Forwarded-For адрес того, кто к нему подключился, поэтому для обычного посетителя оба заголовка называют одного и того же клиента. Они могут различаться для запросов, которые Cloudflare Workers отправляют на ваш исходный сервер: такие запросы тоже уходят с адресов Cloudflare, какому бы аккаунту ни принадлежал Worker. Справочник HTTP-заголовков Cloudflare не говорит, что такой запрос может нести в X-Forwarded-For. Про CF-Connecting-IP говорит: Worker может выбрать значение только для запроса к хосту в собственной зоне; для хоста в другой зоне Cloudflare заголовок содержит фиксированный адрес 2a06:98c0:3600::103, а для хоста не на Cloudflare — адрес клиента, который Worker изменить не может.

Доверие к диапазонам Cloudflare всё равно означает доверие ко всему, что выходит из сети Cloudflare, а не только к вашей зоне. Если адрес клиента влияет на решения о безопасности, сделайте так, чтобы исходный сервер принимал трафик только из вашей зоны, — теми средствами, которые предлагает ваша платформа.

Безопасность

  • Безопасное значение по умолчанию — без TRUSTED_PROXIES заголовки перенаправления не обрабатываются
  • Валидация CIDR — некорректные значения в TRUSTED_PROXIES приводят к ошибке при запуске
  • Устойчивость к подмене — алгоритм rightmost-non-trusted игнорирует добавленные атакующим значения
  • Заголовки перенаправления в запросах с недоверенных IP игнорируются полностью

Промпты для вашего агента

Эти промпты рассчитаны на агента с подключённым MCP-сервером документации OxPHP — нужные страницы он прочитает сам.

prompt
Настрой доверенные прокси для моего развёртывания OxPHP. Прочитай страницу `security/trusted-proxies` через MCP-сервер документации OxPHP. Определи по моим инфраструктурным файлам, что стоит перед приложением, затем дай точную конфигурацию и скажи мне, что впускает каждое значение и что клиент смог бы подделать, ошибись я в нём.

Смотрите также

Нашли ошибку? Сообщите →