CGI-ACL
view release on metacpan or search on metacpan
print "Access denied.\n";
exit;
}
## Sharing a base ACL across routes with cloning
Call `new()` on an existing object to get an independent copy.
Changing the copy does not affect the original.
use CGI::ACL;
# Shared base: block cloud for all routes
my $base_acl = CGI::ACL->new()->deny_cloud();
# Admin route: additionally restrict to a single IP
my $admin_acl = $base_acl->new()->allow_ip('198.51.100.1');
if ($admin_acl->all_denied()) {
print "Content-Type: text/plain\n\n";
print "Admin access denied.\n";
exit;
}
The module optionally integrates with [CGI::Lingua](https://metacpan.org/pod/CGI%3A%3ALingua) for country detection.
Runtime configuration is supported via [Object::Configure](https://metacpan.org/pod/Object%3A%3AConfigure).
# COMMON PITFALLS
The following mistakes are easy to make. Read this section before filing
a bug report.
## allow\_country alone has no effect
`allow_country()` only restricts access when default-deny mode is active.
Default-deny mode is activated by `deny_country('*')` or
`deny_all_countries()`. Without it, `allow_country()` is silently
ignored and everyone is still allowed.
# WRONG -- this allows everyone; allow_country is ignored
my $acl = CGI::ACL->new()->allow_country('US');
# RIGHT -- deny all countries first, then add permitted ones
my $acl = CGI::ACL->new()->deny_all_countries()->allow_country('US');
## deny\_cloud overrides allow\_ip
Cloud detection has the highest priority. An IP address that is listed
in `allow_ip()` is still blocked if its reverse DNS resolves to a cloud
provider hostname. This is intentional: cloud IPs can be reassigned, so
the rDNS check is more reliable than the IP address alone.
# This STILL blocks the IP if it is a cloud host
my $acl = CGI::ACL->new()
->deny_cloud()
->allow_ip('198.51.100.5'); # blocked if rDNS says EC2
## Localhost is not automatically allowed
Once any restriction is set, `127.0.0.1` is subject to the same rules
as any other address. If you need to allow local access (for example,
a health-check endpoint), add it explicitly.
my $acl = CGI::ACL->new()
->allow_ip('127.0.0.1') # must be explicit
->deny_all_countries()
->allow_country('US');
## Forgetting the lingua argument
When country restrictions are active and `all_denied()` is called without
a `lingua` argument, the module emits a `carp` warning and denies the
request. Always pass a `CGI::Lingua` object when country rules are in use.
# WRONG -- will carp and deny every request
my $acl = CGI::ACL->new()->deny_all_countries()->allow_country('US');
$acl->all_denied();
# RIGHT
my $lingua = CGI::Lingua->new(supported => ['en']);
$acl->all_denied(lingua => $lingua);
## VPN and proxy users bypass IP and country checks
A visitor who connects through a VPN, Tor exit node, or anonymous proxy
will appear to come from the proxy's IP address and country, not their
own. CGI::ACL has no way to detect this. Cloud blocking provides some
mitigation for VPS-based proxies.
## Country codes are case-insensitive but stored lowercase
`deny_country('BR')` and `deny_country('br')` are equivalent. All
country codes are stored in lowercase. `CGI::Lingua::country()` may
return either case; `all_denied()` normalises it with `lc()` before
comparing.
## The DNS result cache is not shared between CGI requests
In traditional CGI (one process per request), the per-object DNS cache
is destroyed at the end of every request. The cache is only useful in
persistent-process setups such as FastCGI, mod\_perl, or Plack servers,
where the same `CGI::ACL` object survives across many requests.
# SUBROUTINES/METHODS
## new
Creates and returns a new CGI::ACL object.
When called on an existing object it returns a deep clone of that object,
optionally overriding public fields with the supplied arguments. The public
data hashes (`allowed_ips`, `deny_countries`, `allow_countries`) are
copied so that mutations to the clone do not affect the original.
Derived/private keys (`_cidrlist`, `_cloud_cache`) are always cleared;
they are rebuilt from the cloned public state on the next `all_denied()`
call.
**Security note:** private `_*` keys are stripped from all constructor
arguments, including those supplied via environment variables or a config
file. Accepting `_cloud_cache` entries from outside the process would
allow an attacker with environment-variable access to pre-seed the DNS
result cache and bypass `deny_cloud()` for specific IP addresses.
( run in 1.125 second using v1.01-cache-2.11-cpan-364913b4093 )