CGI-Lingua
view release on metacpan or search on metacpan
t/edge_cases.t view on Meta::CPAN
sub { push @warnings, $_[1] });
my $l = _obj([$LANG{EN}]);
$l->language();
ok((grep { ref($_) ? $_->{warning} =~ /invalid/i : /invalid/i } @warnings),
"SQL injection payload is rejected");
Test::Mockingbird::restore_all();
_block_network();
};
subtest 'HTTP_ACCEPT_LANGUAGE: newline embedded in header is rejected (log-injection guard)' => sub {
# \n IS in \s, which is in the character class, so a naive test might
# accept it. However the `/a` flag combined with $ (end-anchor without
# /m) means that the character-class capture must consume the ENTIRE string
# (including the second line). The colon in "X-Header: value" is NOT in
# the class, so multi-line injection payloads are rejected.
local %ENV = (
HTTP_ACCEPT_LANGUAGE => "en\nX-Injected-Header: value",
REMOTE_ADDR => $IP{LOOPBACK},
);
my @warnings;
Test::Mockingbird::mock('CGI::Lingua', '_warn',
sub { push @warnings, $_[1] });
my $l = _obj([$LANG{EN}]);
$l->language();
ok((grep { ref($_) ? $_->{warning} =~ /invalid/i : /invalid/i } @warnings),
'Header-injection payload with colon is rejected');
Test::Mockingbird::restore_all();
_block_network();
};
subtest 'HTTP_ACCEPT_LANGUAGE: Unicode content rejected by /a flag' => sub {
# The /a flag restricts \w, \d, \s to ASCII-only, blocking multi-byte
# Unicode that would otherwise match [A-Za-z0-9].
# Setting Unicode in %ENV produces a "Wide character in setenv" warning on
# some platforms; suppress it so the test is portable.
local %ENV = (REMOTE_ADDR => $IP{LOOPBACK});
{ local $SIG{__WARN__} = sub {};
$ENV{HTTP_ACCEPT_LANGUAGE} = "zh-\x{4e2d}\x{6587}" } # zh-䏿
my @warnings;
Test::Mockingbird::mock('CGI::Lingua', '_warn',
sub { push @warnings, $_[1] });
my $l = _obj([$LANG{EN}]);
$l->language();
ok((grep { ref($_) ? $_->{warning} =~ /invalid/i : /invalid/i } @warnings),
'Unicode in Accept-Language is rejected (ASCII-only mode)');
Test::Mockingbird::restore_all();
_block_network();
};
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# SECTION 3: REMOTE_ADDR injection and boundary conditions
#
# Strategy: probe the IP-validation regex
# IPv4: /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/a
# IPv6: /^([0-9a-fA-F:]{2,39})$/a
# and the subsequent Data::Validate::IP checks.
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
subtest 'REMOTE_ADDR: command injection rejected before any geo lookup' => sub {
# The semicolon is not in either IP regex, so this is blocked at the
# untaint step â no geo module or shell is ever called.
local %ENV = (REMOTE_ADDR => '8.8.8.8;rm -rf /');
my @warnings;
Test::Mockingbird::mock('CGI::Lingua', '_warn',
sub { push @warnings, $_[1] });
my $l = _obj([$LANG{EN}]);
my $cc = $l->country();
ok(!defined $cc, 'Command-injection REMOTE_ADDR returns undef');
ok((grep { ref($_) ? $_->{warning} =~ /valid IP/i : /valid IP/i } @warnings),
'_warn fired for injection attempt in REMOTE_ADDR');
Test::Mockingbird::restore_all();
_block_network();
};
subtest 'REMOTE_ADDR: path traversal rejected' => sub {
local %ENV = (REMOTE_ADDR => '../etc/passwd');
my $l = _obj([$LANG{EN}]);
ok(!defined $l->country(), 'Path traversal REMOTE_ADDR returns undef');
};
subtest 'REMOTE_ADDR: out-of-range octet handled by Data::Validate::IP' => sub {
# "999.1.1.1" matches \d{1,3} (each octet can be 1-3 digits) but
# Data::Validate::IP::is_ipv4 rejects octets > 255.
local %ENV = (REMOTE_ADDR => '999.1.1.1');
my @warnings;
Test::Mockingbird::mock('CGI::Lingua', '_warn',
sub { push @warnings, $_[1] });
my $l = _obj([$LANG{EN}]);
my $cc = $l->country();
ok(!defined $cc,
'Out-of-range octet address returns undef');
Test::Mockingbird::restore_all();
_block_network();
};
subtest 'REMOTE_ADDR: SQL injection in IP field rejected' => sub {
local %ENV = (REMOTE_ADDR => "1.2.3.4'; DROP TABLE users--");
my @warnings;
Test::Mockingbird::mock('CGI::Lingua', '_warn',
sub { push @warnings, $_[1] });
my $l = _obj([$LANG{EN}]);
ok(!defined $l->country(), 'SQL injection in REMOTE_ADDR returns undef');
Test::Mockingbird::restore_all();
_block_network();
};
subtest 'REMOTE_ADDR: very long string is rejected before geo lookup' => sub {
# An overlong string cannot match the tightly-bounded IPv4/IPv6 patterns.
local %ENV = (REMOTE_ADDR => ('1' x 1000) . '.1.1.1');
my $l = _obj([$LANG{EN}]);
ok(!defined $l->country(), 'Overlong REMOTE_ADDR returns undef without crash');
};
subtest 'REMOTE_ADDR: IPv6 injection with trailing semicolon rejected' => sub {
# Semicolon is not in [0-9a-fA-F:], so this never makes it to geo lookup.
local %ENV = (REMOTE_ADDR => '2001:db8::1;ls');
my @warnings;
Test::Mockingbird::mock('CGI::Lingua', '_warn',
sub { push @warnings, $_[1] });
my $l = _obj([$LANG{EN}]);
t/edge_cases.t view on Meta::CPAN
_block_network();
};
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# SECTION 6: Upstream geo-lookup failure returns
#
# Strategy: mock IP::Country to return every documented "bad" value and verify
# that country() handles each gracefully â warning where documented, returning
# the right remapped value, or falling through to the next geo module.
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
subtest 'geo: IP::Country returns undef â country() falls through' => sub {
local %ENV = (REMOTE_ADDR => $IP{PUBLIC});
my $l = _obj([$LANG{EN}]);
_inject_ipcountry($l, undef);
# No subsequent geo module is available â should reach whois (mocked no-op).
my $cc = $l->country();
ok(!defined $cc, 'country() returns undef when IP::Country returns undef and fallbacks empty');
{ local $SIG{__WARN__} = sub {}; Test::Mockingbird::restore_all() }
_block_network();
};
subtest 'geo: IP::Country returns empty string â treated as undef' => sub {
local %ENV = (REMOTE_ADDR => $IP{PUBLIC});
my $l = _obj([$LANG{EN}]);
_inject_ipcountry($l, '');
my $cc = $l->country();
ok(!defined $cc || $cc eq '',
'Empty string from IP::Country does not crash country()');
{ local $SIG{__WARN__} = sub {}; Test::Mockingbird::restore_all() }
_block_network();
};
subtest 'geo: IP::Country returns numeric "1" â discarded with warning' => sub {
# POD message: "IP matches to a numeric country"
local %ENV = (REMOTE_ADDR => $IP{PUBLIC});
my @warnings;
Test::Mockingbird::mock('CGI::Lingua', '_warn',
sub { push @warnings, $_[1] });
my $l = _obj([$LANG{EN}]);
_inject_ipcountry($l, '1');
my $cc = $l->country();
ok(!defined $cc, 'Numeric country from IP::Country returns undef');
ok((grep { ref($_) ? $_->{warning} =~ /numeric/i : /numeric/i } @warnings),
'Warning fired for numeric country from IP::Country');
{ local $SIG{__WARN__} = sub {}; Test::Mockingbird::restore_all() }
_block_network();
};
subtest 'geo: IP::Country returns "eu" â deleted, falls through' => sub {
# The module discards "eu" because it is not a real country code.
local %ENV = (REMOTE_ADDR => $IP{PUBLIC});
my $l = _obj([$LANG{EN}]);
_inject_ipcountry($l, 'EU');
my $cc = $l->country();
# After EU is discarded, fallbacks are all blocked â undef expected.
ok(!defined $cc || ($cc ne 'eu'),
'"eu" from IP::Country is not returned as a country code');
{ local $SIG{__WARN__} = sub {}; Test::Mockingbird::restore_all() }
_block_network();
};
subtest 'geo: IP::Country returns "HK" â remapped to "cn"' => sub {
# POD/code comment: "HK is no longer a separate country in Whois"
local %ENV = (REMOTE_ADDR => $IP{PUBLIC});
my $l = _obj([$LANG{EN}]);
_inject_ipcountry($l, 'HK');
is($l->country(), 'cn', 'HK from IP::Country is remapped to cn');
{ local $SIG{__WARN__} = sub {}; Test::Mockingbird::restore_all() }
_block_network();
};
subtest 'geo: geoplugin returns empty JSON object â country() returns undef' => sub {
SKIP: {
skip 'LWP::Simple::WithCache or JSON::Parse not installed', 1
unless $HAS_LWP && $HAS_JSON;
local %ENV = (REMOTE_ADDR => $IP{PUBLIC});
Test::Mockingbird::mock('LWP::Simple::WithCache', 'get', sub { '{}' });
my $l = _obj([$LANG{EN}]);
$l->{_have_ipcountry} = 0;
$l->{_have_geoip} = 0;
$l->{_have_geoipfree} = 0;
my $cc = $l->country();
# {} has no geoplugin_countryCode key â Whois (no-op mock) is tried next.
ok(!defined $cc, 'Empty JSON from geoplugin results in undef country');
{ local $SIG{__WARN__} = sub {}; Test::Mockingbird::restore_all() }
_block_network();
}
};
subtest 'geo: geoplugin returns malformed JSON â country() survives' => sub {
SKIP: {
skip 'LWP::Simple::WithCache or JSON::Parse not installed', 1
unless $HAS_LWP && $HAS_JSON;
local %ENV = (REMOTE_ADDR => $IP{PUBLIC});
# Malformed JSON causes JSON::Parse to throw. The eval{} in country()
# must absorb the error and fall through rather than crashing.
Test::Mockingbird::mock('LWP::Simple::WithCache', 'get',
sub { 'NOT VALID JSON {{{' });
my $l = _obj([$LANG{EN}]);
$l->{_have_ipcountry} = 0;
$l->{_have_geoip} = 0;
$l->{_have_geoipfree} = 0;
my $cc;
lives_ok { $cc = $l->country() }
'Malformed geoplugin JSON does not crash country()';
{ local $SIG{__WARN__} = sub {}; Test::Mockingbird::restore_all() }
( run in 1.967 second using v1.01-cache-2.11-cpan-800906f7e73 )