HTML-OSM
view release on metacpan or search on metacpan
t/edge_cases.t view on Meta::CPAN
subtest 'add_choropleth: empty scale array renders without crashing' => sub {
my $m = HTML::OSM->new();
$m->center([$C{LAT_LONDON}, $C{LON_LONDON}]);
my @feats = ({ type => 'Feature', properties => { name => 'A' },
geometry => { type => 'Point', coordinates => [0,0] } });
# Empty scale: color lookup falls to undefined, JS uses '#cccccc' fallback.
lives_ok { $m->add_choropleth(\@feats, { A => 10 }, scale => []) }
'empty scale array does not croak in add_choropleth';
lives_ok { $m->onload_render() }
'onload_render with empty-scale choropleth does not crash';
};
subtest 'security: choropleth scale color with </script> is escaped in output' => sub {
my $m = HTML::OSM->new();
$m->center([$C{LAT_LONDON}, $C{LON_LONDON}]);
my @feats = ({ type => 'Feature', properties => { name => 'A' },
geometry => { type => 'Point', coordinates => [0,0] } });
$m->add_choropleth(
\@feats,
{ A => 50 },
scale => [$C{SCRIPT_CLOSE}], # user-controlled color string
);
my (undef, $body) = $m->onload_render();
unlike($body, qr|</script><script>|,
'</script> in choropleth scale is escaped in rendered body');
};
subtest 'security: choropleth feature key with </script> in values JSON is escaped' => sub {
my $m = HTML::OSM->new();
$m->center([$C{LAT_LONDON}, $C{LON_LONDON}]);
my @feats = ({ type => 'Feature', properties => { name => 'X' },
geometry => { type => 'Point', coordinates => [0,0] } });
$m->add_choropleth(\@feats, { $C{SCRIPT_CLOSE} => 100 });
my (undef, $body) = $m->onload_render();
unlike($body, qr|</script><script>|,
'</script> in choropleth value key is escaped in rendered body');
};
subtest 'security: choropleth feature data with </script> in geometry is escaped' => sub {
# The entire feature blob is JSON-encoded and embedded. A crafted feature
# property containing </script> must not close the script block.
my $m = HTML::OSM->new();
$m->center([$C{LAT_LONDON}, $C{LON_LONDON}]);
my @feats = ({
type => 'Feature',
properties => { name => $C{SCRIPT_CLOSE} },
geometry => { type => 'Point', coordinates => [0,0] },
});
$m->add_choropleth(\@feats, { $C{SCRIPT_CLOSE} => 10 });
my (undef, $body) = $m->onload_render();
unlike($body, qr|</script><script>|,
'</script> in choropleth feature properties is escaped in rendered body');
};
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# 9. onload_render() â security: geocoder lat/lon injection
#
# VULNERABILITY: when coordinates are seeded via the constructor with undef
# lat/lon, onload_render() geocodes them at render time. The returned lat/lon
# were NOT validated before being embedded in the JS string.
# Attack vector: a compromised or MITM'd Nominatim endpoint returns a crafted
# lat string containing JS â it would be embedded literally in L.marker([lat,lon]).
# Fix: call _validate() on every geocoder result in the onload_render() loop.
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
subtest 'security: geocoder-returned lat/lon validated before JS embedding' => sub {
# Inject a geocoder that returns a JS-injection payload in the lat field.
# A second valid static marker (pre-supplied numeric coords) ensures the map
# still has something to render after the malicious coord is discarded.
mock 'ECJSInjGeo::geocode' => sub {
{ lat => $C{JS_PAYLOAD}, lon => '0' }
};
my $m = HTML::OSM->new(
geocoder => bless({}, 'ECJSInjGeo'),
# First tuple: undef lat/lon â geocoded in onload_render â malicious result
# Second tuple: valid static coord â always accepted
coordinates => [
[undef, undef, 'SomeTown', undef],
[$C{LAT_LONDON}, $C{LON_LONDON}, 'London', undef],
],
);
# With the fix, the malicious coord is rejected; London renders normally.
my (undef, $body) = $m->onload_render();
unlike($body, qr/alert\("edge-case-xss"\)/,
'malicious geocoder lat not embedded raw in rendered JS');
like($body, qr/$C{LAT_LONDON}/, 'valid London marker still present after bad coord discarded');
diag("body excerpt: " . substr($body, 0, 400)) if $ENV{TEST_VERBOSE};
};
subtest 'security: all geocoded coords malicious â map croaks (no valid markers)' => sub {
# When every coord in the constructor is geocoded to a malicious string,
# _validate rejects all of them. No valid markers remain; no explicit center
# was given; the center-computation path ends in a croak.
mock 'ECJSInjGeo2::geocode' => sub {
{ lat => $C{JS_PAYLOAD}, lon => '0' }
};
my $m = HTML::OSM->new(
geocoder => bless({}, 'ECJSInjGeo2'),
coordinates => [[undef, undef, 'TownX', undef]],
);
# center() must be called because no valid markers remain after validation.
throws_ok { $m->onload_render() }
qr/center\(\) must be called/i,
'all-malicious geocoded coords discarded â croak about missing center';
};
subtest 'security: numeric geocoder result passes through and is embedded correctly' => sub {
# Confirm that valid (numeric) geocoder results still render after the fix.
mock 'ECValidGeo::geocode' => sub { { lat => '51.5', lon => '-0.1' } };
my $m = HTML::OSM->new(
geocoder => bless({}, 'ECValidGeo'),
coordinates => [[undef, undef, 'London', undef]],
);
my (undef, $body) = $m->onload_render();
like($body, qr/51\.5/, 'valid geocoded lat present in rendered body');
};
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# 10. onload_render() â other hostile scenarios
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
( run in 1.304 second using v1.01-cache-2.11-cpan-81fc1098f69 )