Alpha ZealPHP is early-stage and under active development. APIs may change between minor versions until v1.0. Feedback and bug reports welcome on GitHub.
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:

  1. If deny matches the IP → 403 (deny wins ties)
  2. Else if allow is non-empty and doesn't match → 403
  3. 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

$allow  : array<string|int, string>
$deny  : array<string|int, string>

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

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
ResponseInterface

cidrMatch()

private cidrMatch(string $ip, string $cidr) : bool
Parameters
$ip : string
$cidr : string
Return values
bool

clientIp()

private clientIp(ServerRequestInterface $request) : string
Parameters
$request : ServerRequestInterface
Return values
string

forbidden()

private forbidden() : ResponseInterface
Return values
ResponseInterface

matchesAny()

private matchesAny(string $ip, array<string|int, string> $rules) : bool
Parameters
$ip : string
$rules : array<string|int, string>
Return values
bool

normalizeIp()

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
Return values
string
On this page