PHPackages                             ahmadrezaei/laravel-real-client-ip - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Security](/categories/security)
4. /
5. ahmadrezaei/laravel-real-client-ip

ActiveLibrary[Security](/categories/security)

ahmadrezaei/laravel-real-client-ip
==================================

Laravel middleware that restores the real client IP behind a CDN, load balancer or ingress controller that overwrites X-Forwarded-For, verified with a shared secret so the header cannot be spoofed.

v1.0.0(today)00MITPHPPHP ^8.2CI passing

Since Jul 31Pushed todayCompare

[ Source](https://github.com/ahmadrezaei/laravel-real-client-ip)[ Packagist](https://packagist.org/packages/ahmadrezaei/laravel-real-client-ip)[ Docs](https://github.com/ahmadrezaei/laravel-real-client-ip)[ RSS](/packages/ahmadrezaei-laravel-real-client-ip/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (6)Versions (2)Used By (0)

Laravel Real Client IP
======================

[](#laravel-real-client-ip)

Trust a CDN-supplied client IP header — but only for requests that also carry a matching shared origin token.

[![Tests](https://github.com/ahmadrezaei/laravel-real-client-ip/actions/workflows/tests.yml/badge.svg)](https://github.com/ahmadrezaei/laravel-real-client-ip/actions/workflows/tests.yml)

The problem
-----------

[](#the-problem)

A CDN edge sits in front of your application. The edge knows the real client IP. Between the edge and your app sits an ingress controller (or a load balancer, or a service mesh) that **overwrites** the standard headers with its own peer address. From a live ingress-nginx configuration:

```
proxy_set_header X-Real-IP        $remote_addr;
proxy_set_header X-Forwarded-For  $remote_addr;
```

Note that this is `proxy_set_header ... $remote_addr`, not `$proxy_add_x_forwarded_for`. Whatever the edge put in those headers is gone by the time the request reaches PHP, and `$request->ip()` reports the ingress pod, forever.

The fix is for the CDN to carry the address in a header the ingress does not manage — the same role `CF-Connecting-IP`plays for Cloudflare. This package defaults to `Mit-Connecting-IP`, and any name works.

But a header the ingress does not manage is also a header the ingress does not *sanitise*. Anyone who reaches your origin directly can set it. So the header is only believed when the request also presents a shared secret that the CDN injects, in `X-Origin-Token` by default.

What it is, and what it is not
------------------------------

[](#what-it-is-and-what-it-is-not)

This is a **trust gate**, not access control:

RequestResultValid token + valid IP header`$request->ip()` returns the CDN-supplied addressValid token + junk IP headerServed normally, the junk is discarded, peer address is usedWrong or missing token**Served normally** using the genuine peer addressIt fails *open* for availability and *closed* for trust. A request that cannot prove where it came from does not get blocked — it simply does not get to choose what its own IP address is. If you genuinely need the origin to refuse direct traffic, [strict mode](#strict-mode) exists and is off by default.

The package is also **a complete no-op until a token is configured**, so it is safe to install, register and deploy before the secret exists.

Requirements
------------

[](#requirements)

- PHP 8.2+
- Laravel 12 or 13

Laravel 11 is deliberately not in the constraint. Every published `11.x` release is currently subject to unresolved Packagist security advisories, so Composer refuses to install it under its default advisory policy — claiming support for a version that cannot be installed would be a lie. The code itself has no Laravel 12-only API in it; if you need 11 and you are prepared to override the advisory policy, widening the constraint in a fork should be all it takes.

Installation
------------

[](#installation)

```
composer require ahmadrezaei/laravel-real-client-ip
```

The service provider is auto-discovered. Publish the config if you want to edit it directly:

```
php artisan vendor:publish --tag=cdn-client-ip-config
```

Configuration
-------------

[](#configuration)

Everything is env-driven. The minimum viable setup is one variable:

```
CDN_CLIENT_IP_TOKENS="a-long-random-shared-secret"
```

The full set:

```
CDN_CLIENT_IP_ENABLED=true                     # master switch
CDN_CLIENT_IP_TOKENS=                          # one token, or several separated by commas
CDN_CLIENT_IP_TOKEN_HEADER="X-Origin-Token"    # header carrying the shared secret
CDN_CLIENT_IP_HEADER="Mit-Connecting-IP"       # header carrying the client address
CDN_CLIENT_IP_MODE="remote_addr"               # remote_addr | forwarded
CDN_CLIENT_IP_REWRITE_FORWARDED=true           # also rewrite X-Forwarded-For / X-Real-IP
CDN_CLIENT_IP_ALLOW_PRIVATE=true               # accept private and reserved ranges
CDN_CLIENT_IP_STRIP_TOKEN_HEADER=true          # hide the secret from the rest of the request
CDN_CLIENT_IP_STRICT=false                     # reject requests without a valid token
CDN_CLIENT_IP_STRICT_STATUS=403
CDN_CLIENT_IP_AUTO_REGISTER=true               # prepend the middleware automatically
```

Generate a token with something like `php -r 'echo bin2hex(random_bytes(32));'`. Treat it as a credential.

Middleware registration and ordering
------------------------------------

[](#middleware-registration-and-ordering)

By default the middleware registers itself at the **front of the global stack**, ahead of `TrustProxies` and ahead of everything else. That is deliberate, and it is the single most important detail in this package.

Middleware that runs before it sees the *ingress* address. Rate limiting, session handling, request logging, `TrustHosts`, firewall packages and your own audit trail all read `$request->ip()`. If the address is fixed up halfway down the stack, half your application keys off one IP and half off another — quietly, and only in production.

If you prefer to register it yourself, set `CDN_CLIENT_IP_AUTO_REGISTER=false` and use `prepend()`:

```
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->prepend(\ManageIt\CdnClientIp\Http\Middleware\TrustCdnClientIp::class);
})
```

`prepend()`, not `append()` and not `web()`. `append()` puts it behind `TrustProxies` and behind everything else that has already made up its mind about the client IP. There is a test in this repository (`test_appending_it_leaves_earlier_middleware_looking_at_the_ingress`) that exists purely to demonstrate that failure.

A `cdn-client-ip` route middleware alias is also registered, for the rare case where only a few routes need it.

Which mode should I use?
------------------------

[](#which-mode-should-i-use)

There are exactly two ways to make `$request->ip()` return the CDN-supplied address, and neither is free.

### `remote_addr` (default)

[](#remote_addr-default)

Overwrites `REMOTE_ADDR` on the request's server bag, and — because `rewrite_forwarded_headers` defaults to true — also rewrites `X-Forwarded-For` and `X-Real-IP` with the same address.

- **Works no matter how the host application configures trusted proxies**, including not configuring them at all.
- Sidesteps Symfony's own trusted-proxy model rather than working through it.

The forwarded-header rewrite is not decoration. Symfony resolves the client IP like this:

```
$ip = $this->server->get('REMOTE_ADDR');
if (! $this->isFromTrustedProxy()) { return [$ip]; }
return $this->getTrustedValues(self::HEADER_X_FORWARDED_FOR, $ip) ?: [$ip];
```

If the app trusts its proxies (`TrustProxies::at('*')`, which is extremely common) then `X-Forwarded-For` wins over `REMOTE_ADDR`. Setting `REMOTE_ADDR` alone would be silently beaten by the ingress-written `X-Forwarded-For`. With the rewrite on, both resolution paths agree and the answer is the same either way. Turning `CDN_CLIENT_IP_REWRITE_FORWARDED` off re-introduces exactly that bug; there is a test pinning that down too.

### `forwarded`

[](#forwarded)

Rewrites `X-Forwarded-For` and `X-Real-IP` only, and lets Symfony's existing trusted-proxy machinery resolve them. `REMOTE_ADDR` is never touched.

- Framework-native: the client IP is still resolved by the mechanism Laravel documents.
- **It does nothing at all unless the app already trusts the immediate peer.** With no trusted proxies configured, `isFromTrustedProxy()` is false, Symfony never looks at the header, and `$request->ip()` keeps returning the ingress address. No error, no warning — the middleware runs, rewrites the header, and has no effect.

### Why `remote_addr` is the default

[](#why-remote_addr-is-the-default)

Because this package gets installed into applications whose proxy configuration the package author cannot see. A default that works everywhere and is slightly impure beats a default that is pure and silently broken in an unknown fraction of installations. The failure mode of `forwarded` mode — "I installed it, nothing changed, there is nothing in the logs" — is the worst kind.

Choose `forwarded` if your app already has a deliberate, correct `TrustProxies` configuration and you would rather keep one single mechanism resolving client IPs. Otherwise leave the default alone.

app trusts proxiesapp trusts nothing`remote_addr` (default)worksworks`remote_addr`, rewrite off**broken** (ingress XFF wins)works`forwarded`works**silent no-op**The CDN side
------------

[](#the-cdn-side)

The edge must **set** both headers on every proxied request. For nginx:

```
location / {
    proxy_set_header Mit-Connecting-IP $remote_addr;
    proxy_set_header X-Origin-Token    "a-long-random-shared-secret";

    proxy_pass https://origin.example.com;
}
```

`proxy_set_header` replaces any client-supplied value, which is the property the whole design depends on — see the security notes. For Cloudflare Workers or a similar edge, set the same two headers on the outbound `fetch`, and delete any inbound copy first.

Strict mode
-----------

[](#strict-mode)

```
CDN_CLIENT_IP_STRICT=true
```

Every request without a valid token is rejected with `403` (configurable via `CDN_CLIENT_IP_STRICT_STATUS` and `CDN_CLIENT_IP_STRICT_MESSAGE`). This turns the trust gate into access control and should be a deliberate decision: the origin stops answering the moment the CDN's token drifts, a certificate probe hits it, or a health check forgets the header.

Strict mode is inert while no token is configured. A missing secret can never take the origin offline.

Security notes
--------------

[](#security-notes)

**The origin connection must be HTTPS.** The token travels in a request header on every single request. Over plaintext HTTP anyone on the path can read it, replay it, and forge any client IP they like. If the CDN-to-origin hop is not TLS, this package provides no security at all — it just moves the spoofable header around.

**This is a shared secret, not mTLS.** Anyone who can administer the CDN configuration for the domain can read the token, and so can anyone who can read the origin's environment. There is no per-request signature, no nonce and no replay protection: a captured token is valid until it is rotated. If your threat model needs cryptographic proof of origin, use mutual TLS or a signed, time-bounded header — not this.

**Only enable this if the CDN overwrites the client-IP header on every proxied request.** If the edge merely appends to it, or passes a client-supplied value through, then a client can put anything it likes in `Mit-Connecting-IP` and the token is the *only* thing between an attacker and a forged IP in your logs, your rate limiter, your geo-blocking and your audit trail. Verify with a request that sets the header itself:

```
curl -H 'Mit-Connecting-IP: 1.2.3.4' https://your-site.example/
```

The address your application records must not be `1.2.3.4`.

**The address is always validated.** Only something `FILTER_VALIDATE_IP` accepts (IPv4 or IPv6) is ever trusted, so even a leaked token cannot inject newlines, hostnames, CIDR ranges or SQL-ish payloads into your logs, rate-limiter keys or audit trails. Set `CDN_CLIENT_IP_ALLOW_PRIVATE=false` to additionally reject private and reserved ranges.

**The token is removed from the request** once it has been checked (`CDN_CLIENT_IP_STRIP_TOKEN_HEADER`), so it does not turn up in exception reports, debug toolbars or request dumps. It can still appear in your web server's access logs if you log request headers — do not.

### Rotating the token

[](#rotating-the-token)

`tokens` accepts a list, so old and new are valid at the same time and there is no window where requests are unauthenticated:

1. Add the new token alongside the old one on the **origin**, and deploy: ```
    CDN_CLIENT_IP_TOKENS="new-secret,old-secret"
    ```
2. Switch the **CDN** to send the new token, and let the change propagate to every edge node.
3. Confirm traffic still resolves real client IPs.
4. Drop the old token on the origin, and deploy: ```
    CDN_CLIENT_IP_TOKENS="new-secret"
    ```

Comparison is timing-safe (`hash_equals`) and does not short-circuit on the first match, so the number of configured tokens is not observable through response timing.

Testing
-------

[](#testing)

```
composer install
composer test
```

The suite runs against Laravel 12 and 13 on PHP 8.2–8.4, at both lowest and highest resolvable dependencies, in CI.

License
-------

[](#license)

MIT. See [LICENSE](LICENSE).

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity45

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/7928105?v=4)[Ahmad Rezaei](/maintainers/ahmadrezaei)[@ahmadrezaei](https://github.com/ahmadrezaei)

---

Top Contributors

[![ahmadrezaei](https://avatars.githubusercontent.com/u/7928105?v=4)](https://github.com/ahmadrezaei "ahmadrezaei (2 commits)")

---

Tags

cdnclient-ipcloudflareingress-nginxip-spoofinglaravellaravel-packagemiddlewarephpreal-ipreverse-proxysecuritytrusted-proxyx-forwarded-forphpmiddlewarelaravelsecuritytrusted proxycloudflarecdnreverse proxyreal ipclient ipREMOTE\_ADDRx-forwarded-foringress-nginxip-spoofing

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/ahmadrezaei-laravel-real-client-ip/health.svg)

```
[![Health](https://phpackages.com/badges/ahmadrezaei-laravel-real-client-ip/health.svg)](https://phpackages.com/packages/ahmadrezaei-laravel-real-client-ip)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3355.4M350](/packages/psalm-plugin-laravel)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

78727.1M203](/packages/laravel-mcp)[laravel/socialite

Laravel wrapper around OAuth 1 &amp; OAuth 2 libraries.

5.7k113.1M948](/packages/laravel-socialite)[laravel/scout

Laravel Scout provides a driver based solution to searching your Eloquent models.

1.7k57.2M655](/packages/laravel-scout)[illuminate/auth

The Illuminate Auth package.

9328.5M1.3k](/packages/illuminate-auth)[illuminate/routing

The Illuminate Routing package.

1419.4M3.3k](/packages/illuminate-routing)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
