The Jerusalem PostThe E1 debate is not just about West Bank settlement construction - it’s about security - opinionESPNLouisiana court rules for the players: Who's affected and what happens nowESPN DeportesAndy Ruiz Jr. cae unánime ante Damian KnybaInquirerDuterte: I was advised that posting bail is better than being detainedRTP DesportoI Liga: Técnico do Rio Ave quer equipa com "caráter e personalidade” frente ao Santa ClaraCBS NewsU.S. strikes Iranian oil tankers in retaliation for Iran targeting warshipsוואלהלאור ביקור וויטקוף וקושנר: פוטין הורה להשהות את התקיפות לשלושה ימיםThe Hollywood ReporterBethenny Frankel Is Turning Burning Man Into Her Non-Reality Shown-tvNeue Ausrichtung für CDU: Merz verabschiedet sich vom Credo "Wohlstand für alle"TagesschauWahl in Sachsen-Anhalt: Welche Koalitionsoptionen gibt es?QuemAtor de ‘Quem Ama Cuida’, João Victor Gonçalves é hostilizado ao sair do 'Rock in Rio'; streamer se desculpaRai NewsSerie A. Fiorentina-Torino 1-2, alle 18 Inter-Napoli e alle 20.45 Roma-Atalanta
The Daily Newsstand · Free, Always
Saturday, September 5, 2026

В логе адрес, с которого запрос не приходил. Его вписал сам клиент

Translate

Недавно разбирал блокировку у заказчика. Сработало правило «десять неудачных входов с одного адреса - бан». Забанило адрес, с которого никто ничего не делал. Через два часа забанило ещё один, потом ещё - и все из документационного диапазона, который в интернете не маршрутизируется.

Логика правила была в порядке. Просто в качестве адреса клиента оно брало то, что клиент про себя написал сам.

Заголовок пишет кто угодно

X-Forwarded-For - обычный HTTP-заголовок. Никакой защиты, подписи или привязки к соединению у него нет; это строка в запросе, и отправить её может любой:

$ curl -s -H 'X-Forwarded-For: 198.51.100.7' http://127.0.0.1:8931/
# на стороне сервера:
XFF: 198.51.100.7

Можно сразу списком, изобразив цепочку прокси, которой не было:

$ curl -s -H 'X-Forwarded-For: 203.0.113.1, 198.51.100.7' http://127.0.0.1:8931/
# на стороне сервера:
XFF: 203.0.113.1, 198.51.100.7

Это не уязвимость сервера и не ошибка curl. Так заголовок и устроен: он существует ровно потому, что за прокси настоящий адрес источника теряется, и кто-то должен его туда положить. Стандарт не описывает способ отличить «положил мой прокси» от «положил клиент», потому что на уровне HTTP такого способа нет.

Что делает прокси

Прокси не заменяет заголовок, а дописывает значение в конец - причём дописывает не свой адрес, а тот, с которого получил соединение (в nginx это $proxy_add_x_forwarded_for). Отсюда и ломается почти всё остальное.

Если клиент прислал X-Forwarded-For: 203.0.113.1, а его запрос прошёл через ваш балансировщик, приложение получит:

X-Forwarded-For: 203.0.113.1, <адрес, с которого балансировщик получил соединение>

Из этого следует, что верить можно только правому концу списка, и то не всему. Каждый ваш прокси в цепочке добавил ровно один элемент справа - и вот эти элементы вы контролируете. Всё, что левее, пришло снаружи и написано неизвестно кем.

Адрес клиента - это N-й элемент с конца, где N - количество ваших собственных прокси на пути. Не первый слева, не последний справа. Условие тут одно: каждый ваш прокси действительно дописывает заголовок - сам nginx без proxy_set_header X-Forwarded-For его не изменяет, и тогда N считается только по тем узлам, которые дописывают.

Ошибка «берём самый левый» встречается чаще прочих, и она полностью управляется клиентом. С «берём самый правый» тоньше: при одном дописывающем прокси там как раз адрес клиента, и до поры всё работает - а при двух и более туда попадает адрес предыдущего прокси, и адреса всех клиентов сливаются в один.

Как это выглядит в конфигурации

В nginx за это отвечает модуль realip. Работают тут две директивы, и про вторую забывают чаще: set_real_ip_from задаёт доверие, real_ip_recursive - как разбирать список.

set_real_ip_from  198.51.100.0/24;   # адреса ваших балансировщиков
set_real_ip_from  203.0.113.10;
real_ip_header    X-Forwarded-For;
real_ip_recursive on;

set_real_ip_from - список тех, кому позволено сообщать адрес клиента. Соединение пришло не с этих адресов - заголовок игнорируется целиком, и $remote_addr остаётся адресом сокета. Именно этот список и делает всю работу; без него real_ip_header означает «верить всем».

real_ip_recursive on предписывает nginx идти по списку справа налево, пропуская доверенные адреса, пока не встретится первый недоверенный, - его и считают адресом клиента. С off берётся ровно один элемент, самый правый; в цепочке из двух ваших прокси там лежит адрес предыдущего прокси, а не клиента.

Рекурсивный разбор безопасен только тогда, когда список set_real_ip_from описывает реальную цепочку. Если в него по привычке внесли всю приватную адресацию 10.0.0.0/8, а приложение доступно ещё и изнутри сети, то любой, кто может обратиться к нему из этой сети, снова управляет результатом.

Отдельно про облачные балансировщики

У управляемого балансировщика обычно нет постоянного адреса - он масштабируется, и адреса меняются. Прописать set_real_ip_from конкретным списком не выйдет, а прописать всю подсеть - значит доверять всему, что в этой подсети запущено, включая соседние сервисы.

Рабочий вариант - не определять доверие по адресу источника, а заставить внешний край отбрасывать то, что прислал клиент. Тогда приложение получит список, целиком составленный вашей инфраструктурой. Такой режим - или его эквивалент в виде отдельного заголовка, который край всегда затирает, - есть у части управляемых балансировщиков; называется он везде по-разному и по умолчанию не включён, потому что по умолчанию цепочка должна сохраняться. Читать документацию своего провайдера тут придётся: у одних это переключатель обработки X-Forwarded-For с режимами «дописать», «сохранить», «удалить», у других задача решена вообще другим заголовком.

Есть и обходной путь: свой заголовок с непредсказуемым именем, который внешний край всегда перезаписывает. Работает, но это соглашение, а не механизм: оно держится ровно до того момента, когда кто-то поставит перед приложением ещё один прокси и не узнает о договорённости.

Что ломается, если адрес взят неверно

Ограничение частоты запросов. Счётчик по подставному адресу не ограничивает никого: значение меняется от запроса к запросу, и каждый запрос выглядит первым. Работает это и в обратную сторону - подставив в заголовок адрес другого пользователя, можно исчерпать чужую квоту.

Списки разрешённых адресов. Проверка «адрес из офисной сети - пускаем в админку» на неправильно взятом заголовке равносильна отсутствию проверки.

Расследование. Адрес в логе неотличим от проверенного значения и переезжает в отчёт без оговорки. Через неделю выясняется, что доверять ему было нельзя, - и переделывать надо весь разбор, а не одну строчку.

Геопривязка и антифрод. Признак «вход из другой страны» считается по тому же полю. Значит, страну входа выбирает тот же, кто шлёт запрос.

Как логировать, чтобы потом не переделывать

Писать оба значения, всегда и рядом. Адрес сокета - это факт, его подделать нельзя без контроля над маршрутизацией. Заголовок - это утверждение. В логе они должны стоять как два разных поля, и по названиям должно быть видно, где что:

log_format  proxied  '$remote_addr $realip_remote_addr "$http_x_forwarded_for" '
                     '$status $request_time "$request"';

После включения realip переменная $remote_addr содержит уже вычисленный адрес клиента, а исходный адрес сокета переезжает в $realip_remote_addr. Сырой заголовок пишется отдельно - он нужен, когда придётся разбираться, почему вычисление дало не то.

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

Что посмотреть у себя

Открыть конфигурацию входного узла и найти real_ip_header. Если он есть, а set_real_ip_from нет или в нём широкая приватная подсеть - это и есть находка.

Посчитать по логам распределение длины цепочки в X-Forwarded-For. Ожидаемое значение - одно и то же число. Разброс означает, что кто-то присылает заголовок сам.

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

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.