web php

Reverse Proxies And Trusted Proxies

A reverse proxy sits in front of the PHP application and forwards requests to it. Common examples include Nginx, Caddy, Apache, load balancers, CDNs, and platform routers.

Proxies can terminate TLS, change the apparent client IP, and add headers such as X-Forwarded-For, X-Forwarded-Proto, and X-Forwarded-Host. PHP must only trust those headers when the request came through a proxy you control.

The problem

Without a proxy, PHP may see the direct client:

Browser -> PHP application

With a proxy, PHP may only see the proxy:

Browser -> CDN/load balancer -> web server -> PHP application

The application may need the original scheme, host, and client IP. Those are often passed in forwarding headers, but clients can fake those headers unless the proxy strips and replaces them.

Trust only known proxies

This small example trusts forwarded data only when the remote address is in an allow-list.

PHP example
<?php

declare(strict_types=1);

function clientIp(array $server, array $trustedProxies): string
{
    $remoteAddress = (string) ($server['REMOTE_ADDR'] ?? '');

    if (in_array($remoteAddress, $trustedProxies, true)) {
        $forwardedFor = (string) ($server['HTTP_X_FORWARDED_FOR'] ?? '');
        $firstIp = trim(explode(',', $forwardedFor)[0] ?? '');

        if ($firstIp !== '') {
            return $firstIp;
        }
    }

    return $remoteAddress;
}

echo clientIp([
    'REMOTE_ADDR' => '10.0.0.10',
    'HTTP_X_FORWARDED_FOR' => '203.0.113.5, 10.0.0.10',
], ['10.0.0.10']) . PHP_EOL;

// Prints:
// 203.0.113.5

Frameworks usually provide trusted proxy configuration. Use that instead of hand-rolling proxy handling in every controller.

Why this matters

Trusted proxy configuration affects:

  • secure URL generation
  • Secure cookies
  • rate limiting by client IP
  • audit logs
  • geo/IP checks
  • redirects to HTTPS
  • host-based routing

Bad configuration can cause insecure cookies, wrong URLs, broken redirects, or rate limits that treat every user as the same proxy IP.

Proxy Chains Need A Clear Rule

X-Forwarded-For can contain a comma-separated chain. The correct client address depends on which proxies your infrastructure trusts and how those proxies rewrite the header.

X-Forwarded-For: client, edge-proxy, internal-proxy

Do not assume the first or last value is always correct in every deployment. Configure the framework with the trusted proxy addresses or ranges, then test the value seen by the application in staging.

Forwarded Host Values

Forwarded host headers affect absolute links, redirects, and password-reset URLs. Treat them as infrastructure-controlled input only after trusted-proxy handling is configured.

For security-sensitive links, prefer an application base URL from trusted configuration when that fits the product. A password-reset email should not contain an attacker-controlled host because the original request supplied an unexpected header.

Test The Real Path

When debugging proxy issues, inspect the request at each boundary:

browser -> CDN/load balancer -> web server -> PHP-FPM -> application

Check which layer terminates TLS, which layer sets forwarding headers, whether untrusted incoming values are removed, and what REMOTE_ADDR reaches PHP.

What you should be able to do

After this lesson, you should be able to explain what a reverse proxy is, name common forwarding headers, reason about proxy chains, understand why forwarded IP, scheme, and host values are dangerous when untrusted, and know that trusted proxy configuration belongs at the framework/server boundary rather than inside random application code.

Practice

Task: Read Client IP Through A Trusted Proxy

Create a PHP helper that reads the client IP safely when a reverse proxy may be present.

Requirements

  • Accept a server array and a list of trusted proxy IPs.
  • Use REMOTE_ADDR when the remote address is not trusted.
  • Use the first X-Forwarded-For IP only when the remote address is trusted.
  • Show one trusted proxy case and one untrusted spoofed-header case.
  • Add a short note explaining why framework trusted-proxy configuration is preferred in real applications.

Check your work

The helper should show the security decision: forwarding headers are data from a trusted proxy only after the proxy itself is trusted.

Show solution
PHP example
<?php

declare(strict_types=1);

function clientIp(array $server, array $trustedProxies): string
{
    $remoteAddress = (string) ($server['REMOTE_ADDR'] ?? '');

    if (!in_array($remoteAddress, $trustedProxies, true)) {
        return $remoteAddress;
    }

    $forwardedFor = (string) ($server['HTTP_X_FORWARDED_FOR'] ?? '');
    $firstIp = trim(explode(',', $forwardedFor)[0] ?? '');

    return $firstIp !== '' ? $firstIp : $remoteAddress;
}

echo clientIp([
    'REMOTE_ADDR' => '10.0.0.10',
    'HTTP_X_FORWARDED_FOR' => '203.0.113.5, 10.0.0.10',
], ['10.0.0.10']) . PHP_EOL;

echo clientIp([
    'REMOTE_ADDR' => '198.51.100.77',
    'HTTP_X_FORWARDED_FOR' => '203.0.113.5',
], ['10.0.0.10']) . PHP_EOL;

// Prints:
// 203.0.113.5
// 198.51.100.77

In a real application, use framework trusted-proxy configuration because it centralises scheme, host, and IP handling. Repeating this logic manually across controllers is easy to get wrong and hard to audit.