Wednesday, November 1, 2017

Can nginx handle duplicate X-Forwarded-For headers?

Leave a Comment

When user using proxy (Google data saver etc), the browser adds X-Forwarded-For for clients' real ip address to server. Our load balancer passes all headers + the clients' ip address as X-Forwarded-For header to nginx server. The example request headers:

X-Forwarded-For: 1.2.3.4 X-Forwarded-Port: 80 X-Forwarded-Proto: http Host: *.*.*.* Accept-Encoding: gzip, deflate, sdch Accept-Language: en-US,en;q=0.8,tr;q=0.6 Save-Data: on Scheme: http Via: 1.1 Chrome-Compression-Proxy X-Forwarded-For: 1.2.3.5 Connection: Keep-alive 

Is there any way to pass both of the X-Forwarded-For headers to php, respectively?

4 Answers

Answers 1

TL;DR

  • nginx: fastcgi_param HTTP_MERGED_X_FORWARDED_FOR $http_x_forwarded_for
  • php: $_SERVER['HTTP_MERGED_X_FORWARDED_FOR']

Explanation

You can access all http headers with the $http_<header_name> variable. When using this variable, nginx will even do header merging for you so

CustomHeader: foo CustomHeader: bar 

Gets translated to the value:

foo, bar 

Thus, all you need to do is pass this variable to php with fastcgi_param

fastcgi_param HTTP_MERGED_X_FORWARDED_FOR $http_x_forwarded_for 

Proof of concept:

in your nginx server block:

location ~ \.php$ {     fastcgi_pass unix:run/php/php5.6-fpm.sock;     fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;     fastcgi_param HTTP_MERGED_X_FORWARDED_FOR $http_x_forwarded_for;     include fastcgi_params; } 

test.php

<?php die($_SERVER['HTTP_MERGED_X_FORWARDED_FOR']); 

And finally see what happens with curl:

curl -v -H 'X-Forwarded-For: 127.0.0.1' -H 'X-Forwarded-For: 8.8.8.8' http://localhost/test.php 

Gives the following response:

* Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 80 (#0) > GET /test.php HTTP/1.1 > Host: localhost > User-Agent: curl/7.47.0 > X-Forwarded-For: 127.0.0.1 > X-Forwarded-For: 8.8.8.8 >  < HTTP/1.1 200 OK < Server: nginx/1.10.3 (Ubuntu) < Date: Wed, 01 Nov 2017 09:07:51 GMT < Content-Type: text/html; charset=UTF-8 < Transfer-Encoding: chunked < Connection: keep-alive <  * Connection #0 to host localhost left intact 127.0.0.1, 8.8.8.8 

Boom! There you go, you have access to all X-FORWARDED-FOR headers, as a comma-delimited string in $_SERVER['HTTP_MERGED_X_FORWARDED_FOR']

Of course, you can use whatever name you want and not just HTTP_MERGED_X_FORWARDED_FOR.

Answers 2

You can get the original client address of the connecting ELB in the variable $realip_remote_addr, but be aware that this variable was only added in nginx 1.9.7, so you'll need to be running a very recent version of nginx.

For more info. ngx_http_realip_module variables

For example, with this config:

set_real_ip_from 127.0.0.1; set_real_ip_from 192.168.2.1; real_ip_header X-Forwarded-For; real_ip_recursive on; 

And an X-Forwarded-For header resulting in:

X-Forwarded-For: 123.123.123.123, 192.168.2.1, 127.0.0.1 

By default, nginx will pick up the leftmost IP 123.123.123.123 as the client's IP address apart from trusted proxies.

But $realip_remote_addr keeps the original client address

Answers 3

The headers for X-Forwarded-For should be appended to by each proxy inline of your request. You should not be getting two headers. Because the values are appended to by design, anyone can add ip's to that list, so do not use it for security checks. If you need to check an ip for security, set the X-Real-IP header on your web server, overwriting any passed in value.

Answers 4

What you are looking for needs to be handled at web server level. So I created two servers one using apache and one using nginx for this. The test command

curl -H "X: Y" -H "X: Z"  http://localhost:8088/router.php | jq 

Apache

When executed using apache the output is below

{   "HEADERS": {     "Host": "localhost:8088",     "User-Agent": "curl/7.47.0",     "Accept": "*/*",     "X": "Y, Z"   } } 

As you can see we passed two headers to apache and apache combined them using ,. If we change our first header to already contain , it would still work fine

$ curl -H "X: Y, A" -H "X: Z"  http://localhost:8088/router.php | jq {   "HEADERS": {     "Host": "localhost:8088",     "User-Agent": "curl/7.47.0",     "Accept": "*/*",     "X": "Y, A, Z"   } } 

Nginx

Now same request on nginx yields

{   "HEADERS": {     "X": "Z",     "Accept": "*/*",     "User-Agent": "curl/7.47.0",     "Host": "localhost"   } } 

Now it is not that Nginx is not sending those headers to PHP-FPM, it does send them as it is. PHP-FPM doesn't merge these duplicate headers into one. So in the script you only get the latest header.

Edit-1: Merge using fastcgi_param

Thanks to @AronCederholm for pointing out that merging does work by specifying FASTCGI_PARAM

I originally had tested the same approach but it had resulted in blank headers. I had tried adding

fastcgi_param X-Forwarded-For $http_x_forwarder_for; 

Just now after reading his message I realized that I had a typo in my config. It should have been

fastcgi_param X-Forwarded-For $http_x_forwarded_for; 

And after this change the header does work fine. It won't come in getallheaders() though. It would be available through $_SERVER[] as shown in below response

$ curl -v -H 'X-Forwarded-For: 127.0.0.1' -H 'X-Forwarded-For: 8.8.8.8' http://localhost/router.php | jq {   "HEADERS": {     "X-Forwarded-For": "8.8.8.8",     "Accept": "*/*",     "User-Agent": "curl/7.47.0",     "Host": "localhost"   },   "SERVER": {     "USER": "vagrant",     "HOME": "/home/vagrant",     "HTTP_X_FORWARDED_FOR": "8.8.8.8",     "HTTP_ACCEPT": "*/*",     "HTTP_USER_AGENT": "curl/7.47.0",     "HTTP_HOST": "localhost",     "X-Forwarded-For": "127.0.0.1, 8.8.8.8", 

Original Answer

Unfortunately I found no settings or plugins for Nginx or PHP-FPM which allows you to merge the duplicate headers into one. And you cannot handle this situation at PHP level, because you will never be able to see the raw headers.

Possible Solutions

  1. Put apache in front of Nginx. Make nginx listen on a unix socket and use apache to reverse proxy the request to nginx
  2. Replace Nginx by Apache
  3. Create a Nginx plugin to merge headers. Below two projects should give you a head start

https://github.com/giom/nginx_accept_language_module

https://github.com/openresty/headers-more-nginx-module

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment