Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nginx pass_proxy subdirectory without url decoding

Tags:

nginx

I need to write an nginx location directive to proxy requests to subdirectory to another server preserving urlencoding and removing subdirectory prefix.

Here's an artificial example — request like this:

http://1.2.3.4/api/save/http%3A%2F%2Fexample.com

should pass as

http://abcd.com/save/http%3A%2F%2Fexample.com

I tried several different ways. Here're couple of them:

  1. From this SO question

     location /api/ {
         rewrite ^/api(/.*) $1 break;
         proxy_set_header   X-Real-IP        $remote_addr;
         proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
         proxy_set_header   Host             $host;
         proxy_pass http://abcd.com;
     }
    

But it decodes the string, so http://abcd.com gets /save/http://example.com

  1. From another SO question

     location /api/ {
         proxy_set_header   X-Real-IP        $remote_addr;
         proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
         proxy_set_header   Host             $host;
         proxy_pass http://abcd.com;
     }
    

But it keeps subdirectory, so http://abcd.com gets /api/save/http%3A%2F%2Fexample.com.

What's needed is somewhere in the middle. Thank you!

UPD: Here's a ticket in nginx bug tracker

like image 698
rinat.io Avatar asked Feb 23 '15 21:02

rinat.io


2 Answers

But there is no easy way to fix this nginx behaviour. There are some bugs in nginx trac, you could add yours. trac.nginx.org/nginx/…. So, I think that the simplest way is to have subdomain. – Alexey Ten Feb 24 '15 at 14:49

https://trac.nginx.org/nginx/ticket/727

If you want nginx to do something custom, you can do so using ​proxy_pass with variables (and the $request_uri variable, which contains original unescaped request URI as sent by a client). In this case it will be your responsibility to do correct URI transformations. Note though that this can easily cause security issues and should be done with care.

Challenge accepted!

    location /api/ {
        rewrite ^ $request_uri;
        rewrite ^/api/(.*) $1 break;
        return 400;
        proxy_pass http://127.0.0.1:82/$uri;
    }

That's it, folks!


Here's for the full proof.

The config file for nginx/1.2.1:

server {
    listen 81;
    #first, the solution
    location /api/ {
        rewrite ^ $request_uri;
        rewrite ^/api/(.*) $1 break;
        return 400; #if the second rewrite won't match
        proxy_pass http://127.0.0.1:82/$uri;
    }
    #next, a few control groups
    location /dec/ {
        proxy_pass http://127.0.0.1:82/;
    }
    location /mec/ {
        rewrite ^/mec(/.*) $1 break;
        proxy_pass http://127.0.0.1:82;
    }
    location /nod/ {
        proxy_pass http://127.0.0.1:82;
    }
}

server {
    listen 82;
    return 200 $request_uri\n;
}

Here are the results of running the queries for each location:

% echo localhost:81/{api,dec,mec,nod}/save/http%3A%2F%2Fexample.com | xargs -n1 curl
/save/http%3A%2F%2Fexample.com
/save/http:/example.com
/save/http:/example.com
/nod/save/http%3A%2F%2Fexample.com
%

Note that having that extra return 400; is quite important — otherwise, you risk having a security issue (file access through //api etc), as Maxim has briefly mentioned in your trac ticket.


P.S. If you think using the rewrite engine as a finite-state automaton is super cool, you might also want check out my http://mdoc.su/ project, or fork it github.

like image 188
cnst Avatar answered Nov 13 '22 21:11

cnst


What you have to do is fairly easy as long as we are talking prefix matching with ^~ or no modifier

location /api/ {
  # if you don't want to pass /api/ add a trailing slash to the proxy_pass
  proxy_pass http://localhost:8080/;

  ...
}

And everything will be passed along without decoding, you don't have to pass $uri

Also while you use proxy pass you should also set these headers

# pass headers and body along
proxy_pass_request_headers on;
proxy_pass_request_body on;

# set some headers to make sure the reverse proxy is passing along everything necessary
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
like image 28
Gradient Avatar answered Nov 13 '22 20:11

Gradient