API Index — Namespaces, Packages, Reports, Indices
IpAccessMiddleware
in package
implements
MiddlewareInterface
IP Access Middleware
Allow/deny request access by client IP. Supports literal IPv4/IPv6 and CIDR
notation (10.0.0.0/8, 2001:db8::/32). The wildcard '*' matches any IP
— useful to express "allow everyone except deny list" or "deny by default".
Apache 2.2 equivalent (legacy):
Order Deny,Allow
Deny from all
Allow from 10.0.0.0/8 127.0.0.1
Apache 2.4+ / nginx equivalent:
Require ip 10.0.0.0/8 127.0.0.1 # Apache 2.4+
allow 10.0.0.0/8; allow 127.0.0.1; deny all; # nginx
Resolution rules:
- If
denymatches the IP →403(deny wins ties) - Else if
allowis non-empty and doesn't match →403 - Else → pass through
So ['allow' => ['10.0.0.0/8'], 'deny' => []] is "allow-list only";
['allow' => ['*'], 'deny' => ['1.2.3.4']] is "deny-list only";
['allow' => ['10.0.0.0/8'], 'deny' => ['10.1.2.3']] is "allow the
subnet except this specific host".
Note on proxied apps (#239): the client IP comes from App::clientIp(), which
honours App::$trusted_proxies + X-Forwarded-For. It is spoof-safe — XFF is
trusted ONLY when the socket peer is inside a configured trusted-proxy CIDR;
otherwise it returns the raw peer. So behind Traefik/Caddy/nginx, set
App::$trusted_proxies to your proxy CIDRs and allow/deny matches the real
client. With no trusted_proxies configured it falls back to the socket peer
(the proxy IP) — same as before — so an over-broad/unset trust list can't be
used to spoof an allow/deny decision.
Usage in app.php:
// Allow only office network and CI
$app->addMiddleware(new \ZealPHP\Middleware\IpAccessMiddleware([
'allow' => ['203.0.113.0/24', '198.51.100.42'],
'deny' => [],
]));
// Block a specific abuser, allow the rest
$app->addMiddleware(new \ZealPHP\Middleware\IpAccessMiddleware([
'allow' => ['*'],
'deny' => ['1.2.3.4'],
]));
Table of Contents
Interfaces
- MiddlewareInterface
Properties
Methods
- __construct() : mixed
- process() : ResponseInterface
- cidrMatch() : bool
- clientIp() : string
- forbidden() : ResponseInterface
- matchesAny() : bool
- normalizeIp() : string
- Collapse an IPv4-mapped IPv6 address (
::ffff:a.b.c.d) to its IPv4 form.
Properties
$allow
private
array<string|int, string>
$allow
$deny
private
array<string|int, string>
$deny
Methods
__construct()
public
__construct([array{allow?: string[], deny?: string[]} $config = [] ]) : mixed
Parameters
- $config : array{allow?: string[], deny?: string[]} = []
process()
public
process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
Parameters
- $request : ServerRequestInterface
- $handler : RequestHandlerInterface
Return values
ResponseInterfacecidrMatch()
private
cidrMatch(string $ip, string $cidr) : bool
Parameters
- $ip : string
- $cidr : string
Return values
boolclientIp()
private
clientIp(ServerRequestInterface $request) : string
Parameters
- $request : ServerRequestInterface
Return values
stringforbidden()
private
forbidden() : ResponseInterface
Return values
ResponseInterfacematchesAny()
private
matchesAny(string $ip, array<string|int, string> $rules) : bool
Parameters
- $ip : string
- $rules : array<string|int, string>
Return values
boolnormalizeIp()
Collapse an IPv4-mapped IPv6 address (::ffff:a.b.c.d) to its IPv4 form.
private
normalizeIp(string $ip) : string
On a dual-stack listener the kernel presents IPv4 peers in this mapped form, so without this an IPv4 allow/deny rule would silently never match the peer — a deny-list bypass. Non-mapped addresses are returned as-is.
Parameters
- $ip : string