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.

Заключение

  1. Веб-приложения, которые напрямую предоставляют сервисы внешнему миру, при выполнении операций, связанных с безопасностью, могут получать IP только через Remote Address и не могут доверять никаким заголовкам запроса;
  2. Веб-приложения, использующие Nginx или другие веб-серверы для обратного прокси, при корректной конфигурации должны использоватьX-Forwarded-Forпоследнюю часть илиX-Real-IPчтобы получить IP (потому что Remote Address получает внутренний IP сервера, на котором расположен Nginx); в то же время прямой внешний доступ к веб-приложению должен быть запрещён;
  3. В сценариях, не связанных с безопасностью, например при отображении местной погоды по 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