File: 04_request_xml.t

package info (click to toggle)
libdancer-perl 1.3521%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 2,460 kB
  • sloc: perl: 7,436; xml: 2,211; sh: 54; makefile: 32; sql: 5
file content (144 lines) | stat: -rw-r--r-- 4,655 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
use Test::More import => ['!pass'];
use strict;
use warnings;
use Dancer ':tests';
use Dancer::ModuleLoader;
use Dancer::Test;

use File::Temp qw(tempfile);
use HTTP::Tiny::NoProxy;
use HTTP::Server::Simple::PSGI;
use Plack::Builder;

my $num_tests = 11;

plan tests => $num_tests;

SKIP: {
    skip 'XML::Simple is needed to run this test', $num_tests
      unless Dancer::ModuleLoader->load('XML::Simple');

    skip 'XML::Parser or XML::SAX are needed to run this test', $num_tests
        unless Dancer::ModuleLoader->load('XML::Parser') or
               Dancer::ModuleLoader->load('XML::SAX');

    set serializer => 'XML', show_errors => 1;

    get '/'          => sub { { foo => 'bar' } };
    post '/'         => sub { request->params };
    get '/error'     => sub { send_error( { foo => 42 }, 401 ) };
    get '/error_bis' => sub { send_error( 42, 402 ) };
    get '/xml'       => sub {
        content_type('text/xml');
        to_xml( { foo => 'bar' } )
    };

    for my $route ( '/', '/xml' ) {
        my $res = dancer_response( GET => $route );
        is $res->header('Content-Type'), 'text/xml';
        like $res->content, qr/<data foo="bar" \/>/;
    }

    my $res = dancer_response(
        POST => '/',
        {
            params  => { foo            => 1 },
            headers => [ 'Content-Type' => 'text/xml' ]
        }
    );
    is_deeply(
        from_xml( $res->content ),
        { foo => 1 },
        "data is correctly deserialized"
    );
    is $res->header('Content-Type'), 'text/xml',
      'goodcontent type set in response';

    $res = dancer_response( GET => '/error' );
    is $res->status, 401;
    is_deeply( from_xml($res->content ), { foo => 42 } );

    $res = dancer_response( GET => '/error_bis' );
    is $res->status, 402;
    is_deeply( from_xml( $res->content ), { error => 42 } );

    # This next test requires us to set up a separate server that we'll
    # hope cunningly-crafted XML doesn't call.
    # FIXME: this was cut and pasted from 24_deployment/01_multi_webapp.t
    # and should be refactored.
    skip "skip test with Test::TCP in win32", 1 if $^O eq 'MSWin32';
    skip "Test::TCP is needed to run this test", 1
        unless Dancer::ModuleLoader->load('Test::TCP' => "1.30");
    skip "Plack is needed to run this test", 1
        unless Dancer::ModuleLoader->load('Plack::Builder');

    # Test::TCP will fork, so we need a temporary file to put shared
    # information.
    my ($temp_fh, $tempfile)
        = tempfile('14_serializer_04_request_xml_XXXXX', TMPDIR => 1);
    Test::TCP::test_tcp(
        client => sub {
            my $port = shift;

            my $ua = HTTP::Tiny::NoProxy->new();
            my $res = $ua->get("http://127.0.0.1:$port/");
            $res = dancer_response(
                POST => '/',
                {
                    content_type => 'text/xml',
                    headers => ['Content-Type' => 'text/xml'],
                    body         => <<XXE_SSRF });
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE wossname [
   <!ENTITY xxe SYSTEM "http://127.0.0.1:$port/" >
]>
<xml>
<xxx>
    &xxe;
</xxx>
</xml>
XXE_SSRF
        },
        server => sub {
            my $port = shift;

            my $app = sub {
                my $env = shift;
                open (my $fh, '>>', $tempfile);
                print $fh "Accessed at " . localtime(time) . "\n";
                close $fh;
                return [200, undef, ['Sure, whatever']];
            };

            $app = builder {
                mount "/" => builder { $app }
            };

            my $server = HTTP::Server::Simple::PSGI->new($port);
            $server->host("127.0.0.1");
            $server->app(builder { $app });
            $server->run;
        },
    );
          
    # XML crafted to call an arbitrary route *from our server*
    # is rejected: we know that the route we set up was called once, by us,
    # deliberately, but it wasn't called as a side-effect of the XML containing
    # an entity that was supposedly "defined" by that URL.
    my @access_lines;
    {
        open (my $fh, '<', $tempfile);
        @access_lines = <$fh>;
        close $fh;
    }

    # As reported in GH #1239, we are seeing a varying number of lines appear:
    # "Accessed at Fri Sep 25 12:02:29 2020"
    # I don't yet know why there are times when we see zero, but we should *always* see more
    # than one. Cover the most common cases for now.
    # TODO: figure out why we sometimes get 0.
    cmp_ok(scalar @access_lines, '>=', 1,
        'No XXE SSRF vulnerability in our XML handling');

    diag( map { "access_line: $_" } @access_lines );
}