CGI-ACL
view release on metacpan or search on metacpan
t/cgi_security.t view on Meta::CPAN
subtest 'ATTACK: wildcard literal "*" returned by lingua->country()' => sub {
# Exploit: if lingua returns '*', and the code compared it against the
# deny_countries wildcard sentinel before lowercasing, an attacker might
# trigger the wildcard-deny branch without being in the allow list.
# All country codes from lingua are treated as plain strings; only the
# explicitly stored wildcard sentinel activates default-deny mode.
my $acl = CGI::ACL->new()->deny_country('*');
my $ling = Test::PenLingua->new('*');
# '*' is falsy-ish only if undef/0/""; '*' is truthy in Perl so it passes
# the `$country = $country_val or return 1` guard. lc('*') = '*'.
# deny_countries->{'*'} IS set (wildcard mode). allow_countries->{'*'}
# is NOT set. Result: deny.
is(denied_at($acl, $SAFE_IP, lingua => $ling), 1,
'wildcard "*" from lingua is not a self-grant; deny_country("*") still enforced');
};
subtest 'ATTACK: very long country code from lingua->country()' => sub {
# Exploit: a 64 KiB country code compared via a hash lookup is O(1) after
# lc(). No ReDoS or OOM risk, but we confirm timing stays sub-second.
my $acl = CGI::ACL->new()->deny_country('cn');
my $ling = Test::PenLingua->new('x' x 65536);
my $start = time();
my $result = denied_at($acl, $SAFE_IP, lingua => $ling);
my $elapsed = time() - $start;
is($result, 0, 'overlong country code is not in deny list => allowed');
ok($elapsed < 2, "overlong country evaluated quickly (${elapsed}s)");
};
# =============================================================================
# GROUP 6 â Cloud hostname injection via mock DNS
#
# An attacker controlling a DNS server could return a hostile PTR record.
# CGI::ACL only does string comparison against @CLOUD_PATTERNS â it never
# passes the hostname to shell or file operations. We verify the RFC 1035
# length guard and that embedded special characters cause no harm.
# =============================================================================
subtest 'ATTACK: PTR record exceeding RFC 1035 253-char limit is rejected' => sub {
# Exploit: a 300-char hostname that embeds a cloud-provider pattern beyond
# the 253-char bound. The length check must fire before any regex matching,
# preventing a compromised resolver from forcing a cloud match via a very
# long hostname.
ok(length($LONG_HOST) > 253, 'precondition: LONG_HOST exceeds 253 chars');
my $long_guard = mock_scoped 'CGI::ACL::_verified_rdns' => sub { $LONG_HOST };
my $acl = CGI::ACL->new()->deny_cloud();
# DNS mock returns $LONG_HOST; RFC 1035 check must reject it => non-cloud => allow
is(denied_at($acl, $CLOUD_IP), 0,
'overlong PTR hostname rejected by RFC 1035 check; IP treated as non-cloud');
};
subtest 'ATTACK: PTR record with shell metacharacters (command injection via hostname)' => sub {
# Exploit: if _is_cloud_host() or any downstream caller ever passed the
# hostname to system() or 2-arg open(), a PTR like
# "ec2.amazonaws.com; id" would execute arbitrary commands.
# CGI::ACL only performs a regex match; the hostname is never shelled.
my $shell_host = 'ec2-1-2-3-4.compute-1.amazonaws.com; cat /etc/passwd';
my $inject_guard = mock_scoped 'CGI::ACL::_verified_rdns' => sub { $shell_host };
my $acl = CGI::ACL->new()->deny_cloud();
my $result = eval { denied_at($acl, $CLOUD_IP) };
ok(!$@, 'shell-metachar PTR hostname does not cause an exception');
pass('no command was executed; test process alive after shell payload in PTR');
};
subtest 'ATTACK: PTR record with CRLF sequence (header-splitting via DNS)' => sub {
# Exploit: \r\n in a PTR record could split HTTP headers if the hostname
# were ever emitted in a response. CGI::ACL does not output the hostname.
# We verify no exception is raised.
my $crlf_host = "ec2.compute-1.amazonaws.com\r\nX-Injected: pwned";
my $crlf_guard = mock_scoped 'CGI::ACL::_verified_rdns' => sub { $crlf_host };
my $acl = CGI::ACL->new()->deny_cloud();
my $result = eval { denied_at($acl, $CLOUD_IP) };
ok(!$@, 'CRLF-embedded PTR hostname does not cause an exception');
};
# =============================================================================
# GROUP 7 â Type confusion attacks on setter methods
#
# Passing unexpected reference types to allow_ip(), deny_country(), and
# allow_country() must produce a carp and return $self â never return undef
# (which would break method chaining with a cryptic "can't call method on
# undefined value" crash).
# =============================================================================
subtest 'ATTACK: arrayref passed to allow_ip() (type confusion)' => sub {
my $acl = CGI::ACL->new();
my $ret;
does_carp(sub { $ret = $acl->allow_ip([$SAFE_IP]) });
is($ret, $acl, 'allow_ip returns $self on arrayref argument (chain preserved)');
};
subtest 'ATTACK: coderef passed to deny_country() (type confusion)' => sub {
my $acl = CGI::ACL->new();
my $ret;
does_carp(sub { $ret = $acl->deny_country(sub { 'CN' }) });
is($ret, $acl, 'deny_country returns $self on coderef argument (chain preserved)');
};
subtest 'ATTACK: typeglob passed to allow_country() (glob injection)' => sub {
my $acl = CGI::ACL->new();
my $ret;
does_carp(sub { $ret = $acl->allow_country(\*STDIN) });
is($ret, $acl, 'allow_country returns $self on glob argument (chain preserved)');
};
# =============================================================================
# GROUP 8 â all_denied() lingua type confusion
#
# The `lingua` argument must be a blessed object with a country() method.
# Passing any other type must carp and return 1 (deny) without an exception.
# =============================================================================
subtest 'ATTACK: scalar string passed as lingua' => sub {
my $acl = CGI::ACL->new()->deny_country('cn');
does_carp(sub {
is(denied_at($acl, $SAFE_IP, lingua => 'not-an-object'), 1,
'scalar lingua causes deny');
});
};
subtest 'ATTACK: unblessed hashref passed as lingua' => sub {
my $acl = CGI::ACL->new()->deny_country('cn');
( run in 2.404 seconds using v1.01-cache-2.11-cpan-14f38c9f855 )