008filter.t 7.13 KB
Newer Older
1 2 3
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
#
5 6
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
7 8 9 10 11 12 13 14 15 16 17 18

#################
#Bugzilla Test 8#
#####filter######

# This test scans all our templates for every directive. Having eliminated
# those which cannot possibly cause XSS problems, it then checks the rest
# against the safe list stored in the filterexceptions.pl file. 

# Sample exploit code: '>"><script>alert('Oh dear...')</script>

use strict;
19
use lib qw(. lib t);
20 21 22

use vars qw(%safe);

23
use Bugzilla::Constants;
24
use Support::Templates;
25
use File::Spec;
26 27 28 29 30 31
use Test::More tests => $Support::Templates::num_actual_files;
use Cwd;

# Undefine the record separator so we can read in whole files at once
my $oldrecsep = $/;
my $topdir = cwd;
32
$/ = undef;
33 34

foreach my $path (@Support::Templates::include_paths) {
35
    $path =~ s|\\|/|g if ON_WINDOWS;  # convert \ to / in path if on windows
36
    $path =~ m|template/([^/]+)/([^/]+)|;
37
    my $lang = $1;
38 39
    my $flavor = $2;

40 41
    chdir $topdir; # absolute path
    my @testitems = Support::Templates::find_actual_files($path);
42
    chdir $topdir; # absolute path
43 44 45 46 47 48 49
    
    next unless @testitems;
    
    # Some people require this, others don't. No-one knows why.
    chdir $path; # relative path
    
    # We load a %safe list of acceptable exceptions.
50
    if (-r "filterexceptions.pl") {
51
        do "filterexceptions.pl";
52
        if (ON_WINDOWS) {
53 54 55 56 57 58
          # filterexceptions.pl uses / separated paths, while 
          # find_actual_files returns \ separated ones on Windows.
          # Here, we convert the filter exception hash to use \.
          foreach my $file (keys %safe) {
            my $orig_file = $file;
            $file =~ s|/|\\|g;
59 60 61 62
            if ($file ne $orig_file) {
              $safe{$file} = $safe{$orig_file};
              delete $safe{$orig_file};
            }
63 64
          }
        }
65 66 67 68 69 70
    }
    
    # We preprocess the %safe hash of lists into a hash of hashes. This allows
    # us to flag which members were not found, and report that as a warning, 
    # thereby keeping the lists clean.
    foreach my $file (keys %safe) {
71 72 73 74 75 76
        if (ref $safe{$file} eq 'ARRAY') {
            my $list = $safe{$file};
            $safe{$file} = {};
            foreach my $directive (@$list) {
                $safe{$file}{$directive} = 0;    
            }
77 78 79 80 81 82
        }
    }

    foreach my $file (@testitems) {
        # There are some files we don't check, because there is no need to
        # filter their contents due to their content-type.
83
        if ($file =~ /\.(pm|txt|png)\.tmpl$/) {
84
            ok(1, "($lang/$flavor) $file is filter-safe");
85 86 87 88 89 90 91 92 93 94 95 96
            next;
        }
        
        # Read the entire file into a string
        open (FILE, "<$file") || die "Can't open $file: $!\n";    
        my $slurp = <FILE>;
        close (FILE);

        my @unfiltered;

        # /g means we execute this loop for every match
        # /s means we ignore linefeeds in the regexp matches
97
        while ($slurp =~ /\[%(?:-|\+|~|=)?(.*?)(?:-|\+|~|=)?%\]/gs) {
98 99 100 101 102
            my $directive = $1;

            my @lineno = ($` =~ m/\n/gs);
            my $lineno = scalar(@lineno) + 1;

103
            if (!directive_ok($file, $directive)) {
104

105 106 107 108 109
              # This intentionally makes no effort to eliminate duplicates; to do
              # so would merely make it more likely that the user would not 
              # escape all instances when attempting to correct an error.
              push(@unfiltered, "$lineno:$directive");
            }
110 111 112 113 114 115
        }  

        my $fullpath = File::Spec->catfile($path, $file);
        
        if (@unfiltered) {
            my $uflist = join("\n  ", @unfiltered);
116
            ok(0, "($lang/$flavor) $fullpath has unfiltered directives:\n  $uflist\n--ERROR");
117 118 119 120 121 122 123 124 125 126
        }
        else {
            # Find any members of the exclusion list which were not found
            my @notfound;
            foreach my $directive (keys %{$safe{$file}}) {
                push(@notfound, $directive) if ($safe{$file}{$directive} == 0);    
            }

            if (@notfound) {
                my $nflist = join("\n  ", @notfound);
127
                ok(0, "($lang/$flavor) $fullpath - filterexceptions.pl has extra members:\n  $nflist\n" . 
128 129 130 131
                                                                  "--WARNING");
            }
            else {
                # Don't use the full path here - it's too long and unwieldy.
132
                ok(1, "($lang/$flavor) $file is filter-safe");
133 134 135 136 137
            }
        }
    }
}

138 139 140 141
sub directive_ok {
    my ($file, $directive) = @_;

    # Comments
142
    return 1 if $directive =~ /^#/;        
143

144 145 146
    # Remove any leading/trailing whitespace.
    $directive =~ s/^\s*//;
    $directive =~ s/\s*$//;
147 148 149

    # Empty directives are ok; they are usually line break helpers
    return 1 if $directive eq '';
150

151 152 153
    # Make sure we're not looking for ./ in the $safe hash
    $file =~ s#^\./##;

154 155 156 157 158 159 160 161
    # Exclude those on the nofilter list
    if (defined($safe{$file}{$directive})) {
        $safe{$file}{$directive}++;
        return 1;
    };

    # Directives
    return 1 if $directive =~ /^(IF|END|UNLESS|FOREACH|PROCESS|INCLUDE|
162
                                 BLOCK|USE|ELSE|NEXT|LAST|DEFAULT|
163
                                 ELSIF|SET|SWITCH|CASE|WHILE|RETURN|STOP|
164
                                 TRY|CATCH|FINAL|THROW|CLEAR|MACRO|FILTER)/x;
165 166 167 168 169 170 171 172 173 174 175 176 177

    # ? :
    if ($directive =~ /.+\?(.+):(.+)/) {
        return 1 if directive_ok($file, $1) && directive_ok($file, $2);
    }

    # + - * /
    return 1 if $directive =~ /[+\-*\/]/;

    # Numbers
    return 1 if $directive =~ /^[0-9]+$/;

    # Simple assignments
178
    return 1 if $directive =~ /^[\w\.\$\{\}]+\s+=\s+/;
179 180 181 182 183 184 185 186 187 188 189 190

    # Conditional literals with either sort of quotes 
    # There must be no $ in the string for it to be a literal
    return 1 if $directive =~ /^(["'])[^\$]*[^\\]\1/;
    return 1 if $directive =~ /^(["'])\1/;

    # Special values always used for numbers
    return 1 if $directive =~ /^[ijkn]$/;
    return 1 if $directive =~ /^count$/;
    
    # Params
    return 1 if $directive =~ /^Param\(/;
191 192 193
    
    # Hooks
    return 1 if $directive =~ /^Hook.process\(/;
194 195

    # Other functions guaranteed to return OK output
196
    return 1 if $directive =~ /^(time2str|url)\(/;
197 198

    # Safe Template Toolkit virtual methods
199
    return 1 if $directive =~ /\.(length$|size$|push\(|unshift\(|delete\()/;
200 201 202 203 204 205 206 207 208 209

    # Special Template Toolkit loop variable
    return 1 if $directive =~ /^loop\.(index|count)$/;
    
    # Branding terms
    return 1 if $directive =~ /^terms\./;
            
    # Things which are already filtered
    # Note: If a single directive prints two things, and only one is 
    # filtered, we may not catch that case.
210 211
    return 1 if $directive =~ /FILTER\ (html|csv|js|base64|css_class_quote|ics|
                                        quoteUrls|time|uri|xml|lower|html_light|
212
                                        obsolete|inactive|closed|unitconvert|
213
                                        txt|html_linebreak|none)\b/x;
214 215 216 217

    return 0;
}

218 219 220
$/ = $oldrecsep;

exit 0;