CVE-2020-36827: Insufficient Information (NVD-CWE-noinfo)
value: The XAO::Web module before 1.84 for Perl mishandles < and > characters in JSON output during use of json-embed in Web::Action.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-03-24T01:15Z
Repository: https://github.com/amaltsev/XAO-Web
Language: Perl
Files changed: Makefile.PL, Web.pm, lib/XAO/DO/Web/Action.pm, t/testcases/Web/Page.pm
Commit date: 2020-09-17 20:22:22 -0700
Commit message: 1.84 Security fix for json-embed in Web::Action

Encoding <> characters in json output

Diff: diff --git a/Makefile.PL b/Makefile.PL
index 3323e5d..a4642be 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -88,15 +88,15 @@ WriteMakefile(
     'NAME'              => 'XAO::Web',
     'VERSION_FROM'      => 'Web.pm',
     'MIN_PERL_VERSION'  => '5.10.1',
     'LICENSE'           => 'artistic',
     'CONFIGURE_REQUIRES'=> {
-        'XAO::Base'                 => 1.27,
+        'XAO::Base'                 => 1.28,
         'ExtUtils::MakeMaker'       => 6.64,
     },
     'TEST_REQUIRES'     => {
-        'XAO::Base'                 => 1.27,
+        'XAO::Base'                 => 1.28,
     },
     'PREREQ_PM'     => {
         'Authen::SASL::SASLprep'    => 1.1,
         'CGI'                       => 4.50,
         'CGI::Cookie'               => 4.50,
@@ -117,11 +117,11 @@ WriteMakefile(
         'MIME::Lite'                => 2.117,
         'POSIX'                     => 1.16,
         'Text::FormatTable'         => 0.01,
         'Time::HiRes'               => 1.9719,
         'Unicode::Normalize'        => 1.18,
-        'XAO::Base'                 => 1.27,
+        'XAO::Base'                 => 1.28,
         'XAO::FS'                   => 1.21,
     },
     META_MERGE => {
         'meta-spec' => { version => 2 },
         resources => {
diff --git a/Web.pm b/Web.pm
index df55bd1..106118e 100644
--- a/Web.pm
+++ b/Web.pm
@@ -13,11 +13,11 @@ use XAO::Errors qw(XAO::Web);
 
 ###############################################################################
 # XAO::Web version number. Hand changed with every release!
 #
 use vars qw($VERSION);
-$VERSION='1.83';
+$VERSION='1.84';
 
 ###############################################################################
 
 =head1 NAME
 
diff --git a/lib/XAO/DO/Web/Action.pm b/lib/XAO/DO/Web/Action.pm
index 2a9511f..6d3cabd 100644
--- a/lib/XAO/DO/Web/Action.pm
+++ b/lib/XAO/DO/Web/Action.pm
@@ -314,11 +314,20 @@ sub display_data ($@) {
         # The trick with embedded JSON is that it is printed as
         # part of a larger page and as such has to be in characters that
         # are then encoded into bytes of the page final encoding.
         # JSON's to_json() call is exactly that, character output.
         #
-        $self->textout(to_json($data));
+        my $json=to_json($data);
+
+        # The data is typically embedded in a <script>...</script> set
+        # of tags, so if the content of a data field has </script> then
+        # that would close the outer script tag allowing that content to
+        # execute in the page context. Bad bad bad.
+        #
+        $json =~ s/([<>])/'\\u' . sprintf('%04x',ord($1))/esg;
+
+        $self->textout($json);
     }
     elsif($format eq 'xml' || $format eq 'xml-embed') {
         my $xml_sub=$self->get_mode_sub('xml',$args->{'xmlmode'} || $args->{'mode'},$args->{'mode'});
 
         my $xml=$xml_sub->($self,$args,{
diff --git a/t/testcases/Web/Page.pm b/t/testcases/Web/Page.pm
index 5a791b1..fbd84c8 100644
--- a/t/testcases/Web/Page.pm
+++ b/t/testcases/Web/Page.pm
@@ -721,10 +721,12 @@ sub test_unicode_transparency {
     my $self=shift;
 
     use utf8;
     binmode STDERR, ':utf8';
 
+    my $byte2122=Encode::encode('utf8',"\x{2122}");
+
     # By default, mainly for backwards compatibility, the template
     # engine operates on bytes, not characters. Thus we expect bytes
     # back even when we supply unicode.
     #
     # When character mode is set in /xao/page/character_mode configuration parameter
@@ -852,22 +854,27 @@ sub test_unicode_transparency {
             template    => qq(<%Page template='&#8482;'%>),
             expect      => Encode::encode_utf8('™'),
         },
         c15c => {
             template    => qq(<%Page template='&#8482;'%>),
-            expect      => Encode::encode_utf8('™'),
+            expect      => $byte2122,
         },
         #
         d1a => {
-            template    => qq(<script><%MyAction datamode='test-alt' arg='Foo\x{2122}' format='json-embed'%></script>),
-            expect      => qr/Foo\x{2122}/,
+            objname     => 'Web::MyAction',
+            args        => { datamode => 'test-alt', format => 'json-embed', arg => qq(Foo\x{2122}) },
+            expect      => qr/Foo$byte2122/,
         },
         d1b => {
             charmode    => 1,
             template    => qq(<script><%MyAction datamode='test-alt' arg='Foo\x{2122}' format='json-embed'%></script>),
             expect      => qr/Foo\x{2122}/,
         },
+        d1c => {
+            template    => qq(<script><%MyAction datamode='test-alt' arg='</script><script>alert(1);' format='json-embed'%></script>),
+            expect      => qr/\\u003c\/script\\u003e\\u003cscript\\u003ealert/,
+        },
         #
         e1 => {
             template    => "<\$FOO\$>\x00\x01\x02",
             args        => {
                 unparsed    => 1,
@@ -926,11 +933,11 @@ sub test_unicode_transparency {
     while(my ($tname,$test)=each %tests) {
         $self->siteconfig->cleanup();
 
         $self->siteconfig->put('/xao/page/character_mode' => $test->{'charmode'});
 
-        my $page=XAO::Objects->new(objname => 'Web::Page');
+        my $page=XAO::Objects->new(objname => $test->{'objname'} || 'Web::Page');
 
         my $template=$test->{'template'};
         my $got=$page->expand({template => $template},$test->{'args'});
         my $expect=$test->{'expect'};
 
@@ -948,11 +955,11 @@ sub test_unicode_transparency {
             $self->assert(Encode::is_utf8($got),                    # will fail if tests include plain ascii
                 "Test $tname - expected characters, got bytes");
         }
 
         if(ref $expect eq 'Regexp') {
-            $self->assert($got =~ $expect,
+            $self->assert($got =~ $expect ? 1 : 0,
                 "Test $tname - expected to match '$expect', got '$got'");
         }
         else {
             $self->assert($got eq $expect,
                 "Test $tname - expected '$expect', got '$got'");
@@ -974,11 +981,11 @@ sub test_expand {
         '<$TEST$>'      => $str,
         '<$TEST/h$>'    => '\'"!@#$%^&amp;*()_-=[]\\&lt;&gt;? ',
         '<$TEST/f$>'    => '\'&quot;!@#$%^&amp;*()_-=[]\\&lt;&gt;? ',
         '<$TEST/q$>'    => '\'%22!@%23$%25^%26*()_-%3d[]\\%3c%3e%3f%20',
         '<$TEST/u$>'    => '\'%22!@%23$%25^%26*()_-%3d[]\\%3c%3e%3f%20',
-        '<$TEST/j$>'    => '\\u0027\\"!@#$%^&*()_-=[]\\\\<>? ',
+        '<$TEST/j$>'    => '\\u0027\\"!@#$%^&*()_-=[]\\\\\\u003c\\u003e? ',
     );
     foreach my $template (keys %ttt) {
         my $got=$page->expand(template => $template,
                               TEST => $str);
         $self->assert($got eq $ttt{$template},

Vulnerable code:         'XAO::Base'                 => 1.27,
        'XAO::Base'                 => 1.27,
        'XAO::Base'                 => 1.27,
$VERSION='1.83';
        $self->textout(to_json($data));
            expect      => Encode::encode_utf8('™'),
            template    => qq(<script><%MyAction datamode='test-alt' arg='Foo\x{2122}' format='json-embed'%></script>),
            expect      => qr/Foo\x{2122}/,
        my $page=XAO::Objects->new(objname => 'Web::Page');
            $self->assert($got =~ $expect,
        '<$TEST/j$>'    => '\\u0027\\"!@#$%^&*()_-=[]\\\\<>? ',
Fixed code:         'XAO::Base'                 => 1.28,
        'XAO::Base'                 => 1.28,
        'XAO::Base'                 => 1.28,
$VERSION='1.84';
        my $json=to_json($data);

        # The data is typically embedded in a <script>...</script> set
        # of tags, so if the content of a data field has </script> then
        # that would close the outer script tag allowing that content to
        # execute in the page context. Bad bad bad.
        #
        $json =~ s/([<>])/'\\u' . sprintf('%04x',ord($1))/esg;

        $self->textout($json);
    my $byte2122=Encode::encode('utf8',"\x{2122}");

            expect      => $byte2122,
            objname     => 'Web::MyAction',
            args        => { datamode => 'test-alt', format => 'json-embed', arg => qq(Foo\x{2122}) },
            expect      => qr/Foo$byte2122/,
        d1c => {
            template    => qq(<script><%MyAction datamode='test-alt' arg='</script><script>alert(1);' format='json-embed'%></script>),
            expect      => qr/\\u003c\/script\\u003e\\u003cscript\\u003ealert/,
        },
        my $page=XAO::Objects->new(objname => $test->{'objname'} || 'Web::Page');
            $self->assert($got =~ $expect ? 1 : 0,
        '<$TEST/j$>'    => '\\u0027\\"!@#$%^&*()_-=[]\\\\\\u003c\\u003e? ',