CGI-ACL

 view release on metacpan or  search on metacpan

README.md  view on Meta::CPAN

        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 )