# An Nginx Port-Forwarding Pitfall

How unrestricted port capture exposed internal services through a public reverse proxy, and why destination ranges matter.

> I recently made a serious mistake while configuring Nginx port forwarding. Here is what happened.

Our game server used WSS, and Nginx needed to forward many paths to backend WebSocket ports. For example, `wss://xxx.com/10000` should reach `ws://xxx.com:10000`. The requested range was around ports 10000–11000. I did not want to write a thousand separate rules, so I found this configuration online:

```
location ~* ^/(?<port>\d+)$ {
    proxy_pass http://127.0.0.1:$port;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header Host $host;
}
```

At first glance, it seemed to work: take the number after `/` and use it as the backend port.

Then a colleague noticed the problem. Although our security group exposed only the intended game-server ports, this rule allowed a user to reach **any local port indirectly through Nginx's public port 80 or 443**. A small deployment might appear to work normally, but scanning could expose sensitive internal services as traffic grew.

## Restrict the destination port

My first thought was to use `if`. I had also encountered the Nginx community's “If Is Evil” discussion about unexpected behavior in some configurations, so I decided to restrict the numeric match instead.

These are the two patterns recorded in the original post:

```
location ~* ^/(?<port>(20[0-9]\{3\}))$ {
    proxy_pass http://127.0.0.1:$port;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header Host $host;
}
```

Or:

```
location ~* ^/(?<port>2[0-1]\d\d\d)$ {
    proxy_pass http://127.0.0.1:$port;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header Host $host;
}
```

The intent was to match only an allowed range and reject other ports, rather than pass arbitrary user-supplied numbers to the upstream.

**Translation note:** the original post describes a 10000–10999 match, but the two restricted patterns above do not implement that range as written. One also escapes the repetition braces. They are preserved here as historical examples; validate the actual pattern and allowed port range before using the configuration.