App-Netdisco
view release on metacpan or search on metacpan
xt/41-script-json-escape.t view on Meta::CPAN
#!/usr/bin/env perl
# The report and search templates embed their result set as a JavaScript
# literal inside a <script> element:
#
# "data": [% results | none %],
#
# Inside a script element the HTML parser stops at the first "</", even one
# sitting inside a JavaScript string, so any "<" that reaches the page from
# stored data can end the element early and turn the rest of the payload into
# live markup. escape_for_script_context() removes that class of problem by
# emitting every "<" as its JSON unicode escape, which JSON.parse and every
# JavaScript engine read back as "<", leaving the data identical.
#
# U+2028 and U+2029 are escaped for a different reason. This JSON is emitted as
# JavaScript source rather than parsed from a string, and both characters are
# line terminators there.
#
# The payloads below are built with Dancer's to_json, not JSON::PP's
# encode_json, because that is what the route handlers call and the two differ
# in a way that matters here: to_json returns a character string holding a
# literal U+2028, while encode_json returns UTF-8 bytes holding e2 80 a8. A
# test written against encode_json would assert on a character that is not
# there and pass without checking anything.
use strict;
use warnings;
BEGIN { $ENV{DANCER_ENVDIR} = '/dev/null'; }
use Test::More 0.88;
BEGIN {
use_ok( 'App::Netdisco::Util::Web', 'escape_for_script_context' );
}
# Dancer's DSL lives in its own package because it exports a `pass` that
# collides with Test::More's and Perl warns about the prototype mismatch.
{
package JSONish;
use Dancer ':syntax';
sub enc { return Dancer::to_json(@_) }
sub dec { return Dancer::from_json(@_) }
}
sub to_json { JSONish::enc(@_) }
sub from_json { JSONish::dec(@_) }
subtest 'escapeForScriptContext__a_closing_script_tag__cannot_end_the_element' => sub {
my $json = to_json( [ { name => 'router-1</script><script>evil()</script>' } ] );
my $safe = escape_for_script_context($json);
unlike( $safe, qr{</}, 'no "</" survives to be seen by the HTML parser' );
like( $safe, qr/\\u003C/, 'the "<" is emitted as its JSON escape' );
};
subtest 'escapeForScriptContext__an_html_comment_open__is_also_neutralised' => sub {
# <!-- and <script inside a script element drive the parser into its
# escaped states, so escaping every "<" rather than only "</" is what makes
# the rule complete.
my $safe = escape_for_script_context( to_json( [ { name => '<!--<script>' } ] ) );
unlike( $safe, qr/<!--/, 'no comment opener survives' );
unlike( $safe, qr/<script/i, 'no script opener survives' );
};
subtest 'escapeForScriptContext__any_payload__parses_back_to_the_same_data' => sub {
# The whole approach rests on this. If escaping is visible to the consumer,
# every table on every page renders wrong.
my $data = [
{ name => 'router-1</script>', descr => '<!-- x -->', vlan => 42 },
{ name => "line\x{2028}separator", descr => "para\x{2029}separator" },
( run in 1.499 second using v1.01-cache-2.11-cpan-b16cb0d3907 )