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 )