Получите реальный IP-адрес клиента на Heroku

В Heroku Cedar я хотел получить IP-адрес клиента. Первая попытка:

ENV['REMOTE_ADDR']

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

ENV['HTTP_X_FORWARDED_FOR']

Но это не совсем безопасно, не так ли?

Если он содержит только одно значение, я беру это. Если он содержит более одного значения (разделенный запятой), я мог бы взять первый.

Но что, если кто-то манипулирует этим значением? Я не могу доверять ENV['HTTP_X_FORWARDED_FOR'], как мог, с помощью ENV['REMOTE_ADDR']. И нет списка доверенных прокси, которые я мог бы использовать.

Но должен быть какой-то способ надежно получить IP-адрес клиента, всегда. Вы знаете кого-то?

В их документы, Heroku описывает, что X-Forwarded-For - это "исходный IP-адрес клиента, подключающегося к маршрутизатору Heroku".

Звучит так, как если бы Heroku мог переписывать X-Forwarded-For исходящим удаленным IP-адресом. Это предотвратит спуфинг, не так ли? Кто-нибудь может это подтвердить?

Ответ 1

От Иакова, Героку, директора по безопасности в то время:

Маршрутизатор не перезаписывает X-Forwarded-For, но гарантирует, что реальное происхождение всегда будет последним элементом в списке.

Это означает, что при обычном доступе к приложению Heroku вы просто увидите свой IP-адрес в заголовке X-Forwarded-For:

$ curl http://httpbin.org/ip
{
  "origin": "123.124.125.126",
}

Если вы попытаетесь подделать IP-адрес, ваше предполагаемое происхождение будет отражено, но - критически - так ваш IP-адрес. Очевидно, что это все, что нам нужно, поэтому есть четкое и безопасное решение для получения IP-адреса клиента на Heroku:

$ curl -H"X-Forwarded-For: 8.8.8.8" http://httpbin.org/ip
{
  "origin": "8.8.8.8, 123.124.125.126"
}

Это как раз противоположно то, что описано в Википедии, кстати.

Реализация PHP:

function getIpAddress() {
    if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
        $ipAddresses = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
        return trim(end($ipAddresses));
    }
    else {
        return $_SERVER['REMOTE_ADDR'];
    }
}

Ответ 2

Я работаю в отделе поддержки Heroku и некоторое время обсуждал это с нашими инженерами по маршрутизации. Я хотел бы опубликовать дополнительную информацию, чтобы прояснить некоторые вещи о том, что происходит здесь.

Пример, приведенный в ответе выше, просто показал IP-адрес клиента последним по совпадению и не гарантирован. Причина не в том, что исходный запрос утверждал, что он пересылал IP-адрес, указанный в заголовке X-Forwarded-For. Когда маршрутизатор Heroku получил запрос, он просто добавил IP-адрес, который напрямую подключался к списку X-Forwarded-For после того, который был введен в запрос. Наш маршрутизатор всегда добавляет IP-адрес, подключенный к AWS ELB перед нашей платформой в качестве последнего IP-адреса в списке. Этот IP-адрес может быть оригинальным (и в случае, когда есть только один IP-адрес, это почти наверняка есть), но в тот момент, когда есть несколько IP-ключей, все ставки отключены. Конвенция всегда должна добавлять последний IP-адрес в цепочку в конец списка (это то, что мы делаем), но в любой точке цепи, цепочка которой может быть изменена, и могут быть вставлены различные IP-адреса. Таким образом, единственным надежным IP-адресом (с точки зрения нашей платформы) является последний IP-адрес в списке.

Чтобы проиллюстрировать, скажем, кто-то инициирует запрос и произвольно добавляет 3 дополнительных IP-адреса в заголовок X-Forwarded-For:

curl -H "X-Forwarded-For: 12.12.12.12,15.15.15.15,4.4.4.4" http://www.google.com

Представьте, что этот IP-адрес машины был 9.9.9.9 и что он должен был пройти через прокси-сервер (например, прокси-сервер университетского городка). Скажем, что у прокси-сервера был IP-адрес 2.2.2.2. Предполагая, что он не настроен на разделение заголовков X-Forwarded-For (чего, скорее всего, не будет), он просто поместит 9.9.9.9 IP в конец списка и передаст запрос Google. На этом этапе заголовок будет выглядеть так:

X-Forwarded-For: 12.12.12.12,15.15.15.15,4.4.4.4,9.9.9.9

Затем этот запрос будет передаваться через конечную точку Google, которая добавит IP-адрес прокси-сервера университета версии 2.2.2.2, поэтому заголовок, наконец, будет выглядеть в журналах Google:

X-Forwarded-For: 12.12.12.12,15.15.15.15,4.4.4.4,9.9.9.9,2.2.2.2

Итак, это IP-адрес клиента? С точки зрения Google это невозможно. На самом деле клиентский IP-адрес составляет 9.9.9.9. Последний IP-адрес равен 2.2.2.2, а первый - 12.12.12.12. Все, что Google будет знать, это то, что IP-адрес 2.2.2.2 определенно правильный, потому что это был IP-адрес, фактически связанный с их сервисом, - но они не знали бы, был ли это первоначальный клиент для запроса или нет из доступных данных. Точно так же, когда в этом заголовке есть только один IP-адрес - это IP-адрес, который напрямую связан с нашим сервисом, поэтому мы знаем его достоверно.

С практической точки зрения, этот IP, скорее всего, будет надежным большую часть времени (потому что большинство людей не будет пытаться подделать их IP). К сожалению, невозможно предотвратить подобную подделку, и к тому времени, когда запрос дойдет до маршрутизатора Heroku, нам не удалось определить, были ли изменены IP-адреса в цепочке X-Forwarded-For или нет.

Все проблемы с безопасностью, эти IP-цепи всегда должны читаться слева направо. IP-адрес клиента всегда должен быть самым левым IP-адресом.

Ответ 3

Вы никогда не сможете доверять любой информации, поступающей от клиента. Это скорее вопрос о том, кому вы доверяете, и как вы его проверяете. Даже Heroku может быть оказано влияние, чтобы обеспечить плохую ценность HTTP_X_FORWARDED_FOR, если у них есть ошибка в коде, или они каким-то образом взломаны. Другим вариантом может быть еще одна машина Heroku, подключаемая к вашему серверу внутри и обходя свой прокси-сервер вообще при подделке REMOTE_ADDR и/или HTTP_X_FORWARDED_FOR.

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