X-Forwarded-For — это заголовок расширения HTTP. Протокол HTTP/1.1 (RFC 2616) не определяет его. Изначально он был введён Squid, программным обеспечением кэширующего прокси, чтобы представлять реальный IP отправителя HTTP-запроса. Сегодня он стал де-факто стандартом, широко используемым HTTP-прокси, балансировщиками нагрузки и другими службами пересылки, и был включён в стандарт RFC 7239 (Forwarded HTTP Extension).
Формат заголовка запроса X-Forwarded-For очень прост, выглядит так:
X-Forwarded-For: client, proxy1, proxy2
Как видите, содержимое XFF состоит из нескольких частей, разделённых «запятой + пробелом». Первая — это IP устройства, наиболее удалённого от сервера, далее следует IP каждого прокси-устройства.
Если HTTP-запрос проходит через три прокси Proxy1, Proxy2 и Proxy3, прежде чем достичь сервера, с IP-адресами IP1, IP2 и IP3 соответственно, а реальный IP пользователя — IP0, то согласно стандарту XFF сервер в конечном итоге получит следующую информацию:
X-Forwarded-For: IP0, IP1, IP2
Proxy3 напрямую подключается к серверу. Он добавляет IP2 в XFF, указывая, что пересылает запрос от имени Proxy2. IP3 отсутствует в списке; IP3 можно получить на стороне сервера через поле Remote Address. Мы знаем, что HTTP-соединения основаны на TCP-соединениях. В протоколе HTTP нет понятия IP. Remote Address берётся из TCP-соединения и представляет IP устройства, установившего TCP-соединение с сервером. В этом примере это IP3.
Удалённый адрес невозможно подделать, потому что для установления TCP-соединения требуется трёхстороннее рукопожатие. Если IP-адрес источника подделан, TCP-соединение не может быть установлено, и последующего HTTP-запроса не будет. В разных языках удалённый адрес получают по-разному. Например, в PHP используется $_SERVER["REMOTE_ADDR"], в Node.js — req.connection.remoteAddress, но принцип один и тот же.
Проблема
Имея вышеизложенные базовые знания, давайте начнём разбирать проблему. Я написал очень простой веб-сервер на Node.js для тестирования. Протокол HTTP не зависит от языка. Node.js используется здесь только для удобства; на любом другом языке можно прийти к тому же выводу. То же самое относится к Nginx в этой статье. Если интересно, Apache или другие веб-серверы тоже подойдут.
Следующий код слушает порт 9009 и выводит некоторую информацию после получения HTTP-запроса:
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write('remoteAddress: ' + req.connection.remoteAddress + '\n');
res.write('x-forwarded-for: ' + req.headers['x-forwarded-for'] + '\n');
res.write('x-real-ip: ' + req.headers['x-real-ip'] + '\n');
res.end();
}).listen(9009, '0.0.0.0');
Помимо удалённого адреса и X-Forwarded-For, представленных ранее, в этом коде также есть X-Real-IP — ещё одно пользовательское поле заголовка. X-Real-IP часто используется HTTP-прокси для указания IP-адреса устройства, которое установило с ним TCP-соединение. Этим устройством может быть другой прокси или фактический инициатор запроса. Обратите внимание, что X-Real-IP в настоящее время не является частью какого-либо стандарта. Прокси и веб-приложения могут договориться использовать любой пользовательский заголовок для передачи этой информации.
Теперь можно напрямую обращаться к этому сервису Node.js, используя доменное имя + номер порта, а также настроить обратный прокси Nginx:
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://127.0.0.1:9009/;
proxy_redirect off;
}
Мой Nginx слушает порт 80, поэтому я могу обращаться к сервису, проксируемому Nginx, без указания порта.
Проверим прямой доступ к сервису Node:
curl http://t1.imququ.com:9009/ remoteAddress: 114.248.238.236 x-forwarded-for: undefined x-real-ip: undefined
Поскольку мой компьютер подключился напрямую к сервису Node.js, удалённый адрес — это мой IP. Я не указывал никаких дополнительных пользовательских заголовков, поэтому последние два поля оба имеют значение undefined.
Теперь обратимся к сервису, проксируемому Nginx:
curl http://t1.imququ.com/ remoteAddress: 127.0.0.1 x-forwarded-for: 114.248.238.236 x-real-ip: 114.248.238.236
На этот раз мой компьютер обращается к сервису Node.js через Nginx, и полученный Remote Address на самом деле является локальным IP-адресом Nginx. Две строки в конфигурации Nginx, показанные ранее, сработали, добавив в запрос два пользовательских заголовка:
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
Фактически, в производственных средах веб-приложения обычно разворачиваются вторым способом из вышеперечисленных, что даёт множество преимуществ. Но это создаёт скрытую опасность: многие веб-приложения получают реальный IP-адрес пользователя, беря его из заголовков HTTP-запроса.
Заголовки HTTP-запроса можно произвольно конструировать. Давайте с помощью параметра -H у curl сконструируем X-Forwarded-For и X-Real-IP и снова протестируем.
Прямое обращение к сервису Node.js:
curl http://t1.imququ.com:9009/ -H 'X-Forwarded-For: 1.1.1.1' -H 'X-Real-IP: 2.2.2.2' remoteAddress: 114.248.238.236 x-forwarded-for: 1.1.1.1 x-real-ip: 2.2.2.2
Для веб-приложений X-Forwarded-For и X-Real-IP — это всего лишь два обычных заголовка запроса, поэтому они выводятся как есть без какой-либо обработки. Это показывает, что при развёртывании с прямым подключением, кроме Remote Address, полученного из TCP-соединения, IP-информации, переносимой в заголовках запроса, доверять нельзя.
Обращение к сервису, проксированному через Nginx:
curl http://t1.imququ.com/ -H 'X-Forwarded-For: 1.1.1.1' -H 'X-Real-IP: 2.2.2.2' remoteAddress: 127.0.0.1 x-forwarded-for: 1.1.1.1, 114.248.238.236 x-real-ip: 114.248.238.236
На этот раз Nginx добавляет мой IP после X-Forwarded-For и перезаписывает заголовок запроса X-Real-IP моим IP. Это показывает, что при обработке Nginx последний участок X-Forwarded-For и всё содержимое X-Real-IP нельзя подделать, и их можно использовать для получения IP-адреса пользователя.
IP-адрес пользователя часто используется в сценариях, связанных с веб-безопасностью, например для проверки региона входа пользователя и управления частотой доступа на основе IP. В таких сценариях обеспечение невозможности подделки IP важнее. Основываясь на предыдущих тестах и анализе, для веб-приложений, развёрнутых напрямую для пользователей, необходимо использовать Remote Address, полученный из TCP-соединения. Для веб-приложений, развёрнутых за обратным прокси, таким как Nginx, после правильной настройки поведения Set Header можно использовать X-Real-IP или последний участок X-Forwarded-For, переданный Nginx (фактически они эквивалентны).
Итак, как само веб-приложение определяет, пришёл ли запрос напрямую или был перенаправлен управляемым прокси? Добавление дополнительного заголовка запроса при перенаправлении через прокси — один из способов, но он не очень надёжен, поскольку заголовки запроса слишком легко подделать. Если вы всё же используете этот подход, пользовательский заголовок должен быть достаточно длинным и достаточно редким, а также храниться в безопасности, чтобы не утечь.
Проверка того, является ли Remote Address локальным IP, тоже способ, но он не идеален, потому что при доступе с сервера, где находится Nginx, будь то напрямую или через прокси Nginx, Remote Address всегда 127.0.0.1. Обычно этим можно пренебречь. Более проблематично то, что сервер обратного прокси и само веб-приложение могут быть развёрнуты не на одном сервере. Поэтому более разумный подход — поддерживать список всех IP прокси-серверов. Получив Remote Address, веб-приложение может сравнить его со списком по одному, чтобы определить, как был сделан запрос.
Обычно, чтобы упростить логику, в production-средах блокируют прямой доступ к веб-приложениям по порту и разрешают доступ только через Nginx. Означает ли это, что проблем нет? Не обязательно.
Во-первых, если пользователь действительно обращается к Nginx через прокси, последний сегмент X-Forwarded-For и X-Real-IP получит IP прокси. Сценарии, связанные с безопасностью, могут использовать только это, но в некоторых сценариях, например при показе местной погоды на основе IP, нужно по возможности получить реальный IP пользователя. В этот момент первый IP в X-Forwarded-For может пригодиться. Здесь нам нужно обратить внимание на одну проблему. Давайте используем предыдущий пример для тестирования:
curl http://t1.imququ.com/ -H 'X-Forwarded-For: unknown, <>"1.1.1.1' remoteAddress: 127.0.0.1 x-forwarded-for: unknown, <>"1.1.1.1, 114.248.238.236 x-real-ip: 114.248.238.236
Последняя часть X-Forwarded-For добавляется Nginx, но все предшествующие части приходят из заголовков запроса, которые получил Nginx. Эта часть является пользовательским вводом и полностью не заслуживает доверия. При её использовании нужно быть особенно осторожным. Используйте её, только если она соответствует формату IP; в противном случае она легко может привести к таким уязвимостям безопасности, как SQL-инъекция или XSS.
Заключение
- Веб-приложения, которые напрямую предоставляют сервисы внешнему миру, при выполнении операций, связанных с безопасностью, могут получать IP только через Remote Address и не могут доверять никаким заголовкам запроса;
- Веб-приложения, использующие Nginx или другие веб-серверы для обратного прокси, при корректной конфигурации должны использовать
X-Forwarded-Forпоследнюю часть илиX-Real-IPчтобы получить IP (потому что Remote Address получает внутренний IP сервера, на котором расположен Nginx); в то же время прямой внешний доступ к веб-приложению должен быть запрещён; - В сценариях, не связанных с безопасностью, например при отображении местной погоды по IP, можно получить IP из
X-Forwarded-Forболее ранней позиции, но нужно проверить формат IP.
PS: Некоторые статьи в интернете предлагают настраивать Nginx таким образом, но на самом деле это неразумно:
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr;
При такой конфигурации безопасность действительно повышается, но она также стирает всю информацию о прокси до того, как запрос достигнет Nginx, что делает невозможным предоставление лучших сервисов пользователям, которые действительно используют прокси. Всё равно следует понимать основные принципы и анализировать конкретные сценарии в каждом случае.
Оригинальный адрес: https://imququ.com/post/x-forwarded-for-header-in-http.html