Database-BI
view release on metacpan or search on metacpan
t/cgi_security.t view on Meta::CPAN
->status_is(200)
->content_unlike(qr{root:x:0:0},
'local file content is not returned');
};
subtest '/import -- ftp:// scheme is rejected' => sub {
# Proof: "ftp://" does not match /\Ahttps?:\/\//i.
$t->get_ok('/import?url=' . url_escape('ftp://evil.com/data'))
->status_is(200)
->content_like(qr/not a valid/i,
'ftp:// scheme triggers the invalid-URL error');
};
subtest '/import -- javascript: scheme is rejected' => sub {
# Exploit mechanism: attacker injects a JavaScript URI hoping it executes
# either server-side or is reflected and executed client-side.
$t->get_ok('/import?url=' . url_escape('javascript:alert(1)'))
->status_is(200)
->content_like(qr/not a valid/i);
};
subtest '/import -- empty url param triggers required-error message' => sub {
$t->get_ok('/import?url=')->status_is(200)->content_like(qr/Please enter a URL/i);
};
subtest '/import -- missing url param renders prompt' => sub {
$t->get_ok('/import')
->status_is(200)
->content_like(qr/Please enter a URL/i);
};
# ---------------------------------------------------------------------------
# Attack vector 5b: SSRF via private/loopback/link-local addresses
#
# _is_safe_url() resolves the hostname and rejects RFC 1918 (10/8, 172.16/12,
# 192.168/16), loopback (127/8), link-local (169.254/16), and CGNAT (100.64/10).
# The response must not attempt a network connection; it renders home with an
# error message.
# ---------------------------------------------------------------------------
subtest '/import -- http://localhost/ is rejected (SSRF loopback)' => sub {
# Exploit mechanism: attacker fetches internal-only services on the loopback
# interface (e.g. Redis :6379, internal admin panels, Kubernetes API).
# Proof: _is_safe_url() matches $host eq 'localhost' and returns 0.
$t->get_ok('/import?url=' . url_escape('http://localhost/'))
->status_is(200)
->content_like(qr/private or reserved/i,
'localhost is rejected as a private address');
};
subtest '/import -- http://127.0.0.1/ is rejected (SSRF loopback)' => sub {
# Exploit mechanism: bare loopback IPv4 bypasses hostname-based blocklists.
# Proof: $host =~ /\A127\./ check in _is_safe_url() catches all of 127/8.
$t->get_ok('/import?url=' . url_escape('http://127.0.0.1/'))
->status_is(200)
->content_like(qr/private or reserved/i,
'127.0.0.1 is rejected as a loopback address');
};
subtest '/import -- http://169.254.169.254/ is rejected (SSRF cloud metadata)' => sub {
# Exploit mechanism: AWS/GCP/Azure metadata endpoint returns IAM credentials
# and instance metadata; no authentication is required from the instance.
# Proof: 169.254/16 is in the link-local block checked by _is_safe_url().
$t->get_ok('/import?url=' . url_escape('http://169.254.169.254/latest/meta-data/'))
->status_is(200)
->content_like(qr/private or reserved/i,
'169.254.169.254 (cloud metadata) is rejected');
};
# ---------------------------------------------------------------------------
# Attack vector 6: Join left-spec injection
#
# The l= param is parsed by _open_spec. "table:" accepts only [A-Za-z0-9_]+;
# "path:" requires realpath + EXT_RE; "url:" requires https?:// scheme.
# All other prefixes fall through to the final "return ()" -- 404.
# ---------------------------------------------------------------------------
subtest 'join l= -- path traversal via table: prefix is rejected' => sub {
# Proof: "table:../../etc" contains '.' and '/' which fail [A-Za-z0-9_]+.
$t->get_ok('/join?l=' . url_escape('table:../../etc'))
->status_is(404);
};
subtest 'join l= -- path to /etc/passwd via path: prefix is rejected' => sub {
# Proof: /etc/passwd has no supported extension => EXT_RE fails => 404.
$t->get_ok('/join?l=' . url_escape('path:/etc/passwd'))
->status_is(404);
};
subtest 'join l= -- javascript: url is rejected' => sub {
# Proof: _open_spec url: branch requires /\Aurl:(https?:\/\/.+)\z/i.
$t->get_ok('/join?l=' . url_escape('url:javascript:alert(1)'))
->status_is(404);
};
subtest 'join l= -- unknown spec prefix is rejected' => sub {
# Proof: _open_spec returns () for any spec that doesn't match table:/path:/url:.
$t->get_ok('/join?l=' . url_escape('exec:/usr/bin/id'))
->status_is(404);
};
SKIP: {
skip 'data/sales.csv not found', 1 unless -f $SALES_CSV_PATH;
subtest 'join j= -- path traversal in right-table spec is silently skipped' => sub {
# Exploit mechanism: j= with a path: spec pointing to /etc/passwd.
# Proof: _open_spec rejects /etc/passwd (no ext); step is skipped.
# Left table (sales) still renders.
my $hostile = url_escape('path:/etc/passwd|id|id');
$t->get_ok("/join?l=table:sales&j=$hostile")
->status_is(200);
};
}
# ---------------------------------------------------------------------------
# Attack vector 7: Header injection via Content-Disposition in GET /export
#
# The export_data action sanitizes the filename stem:
# (my $safe_name = lc $left_label) =~ s/[^a-z0-9_]+/_/g
# CRLF characters, angle brackets, and quotes are all replaced by '_'.
# The resulting value is safe to embed in the Content-Disposition header.
( run in 1.649 second using v1.01-cache-2.11-cpan-007c89162af )