| # -*- Mode: perl; indent-tabs-mode: nil -*- |
| # |
| # The contents of this file are subject to the Mozilla Public |
| # License Version 1.1 (the "License"); you may not use this file |
| # except in compliance with the License. You may obtain a copy of |
| # the License at http://www.mozilla.org/MPL/ |
| # |
| # Software distributed under the License is distributed on an "AS |
| # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or |
| # implied. See the License for the specific language governing |
| # rights and limitations under the License. |
| # |
| # The Original Code are the Bugzilla tests. |
| # |
| # The Initial Developer of the Original Code is Jacob Steenhagen. |
| # Portions created by Jacob Steenhagen are |
| # Copyright (C) 2001 Jacob Steenhagen. All |
| # Rights Reserved. |
| # |
| # Contributor(s): Gervase Markham <gerv@gerv.net> |
| |
| ################# |
| #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; |
| use lib 't'; |
| |
| use vars qw(%safe); |
| |
| use Support::Templates; |
| use File::Spec; |
| 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; |
| $/ = undef; |
| |
| foreach my $path (@Support::Templates::include_paths) { |
| $path =~ s|\\|/|g if $^O eq 'MSWin32'; # convert \ to / in path if on windows |
| $path =~ m|template/([^/]+)/([^/]+)|; |
| my $lang = $1; |
| my $flavor = $2; |
| |
| chdir $topdir; # absolute path |
| my @testitems = Support::Templates::find_actual_files($path); |
| chdir $topdir; # absolute path |
| |
| 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. |
| if (!-r "filterexceptions.pl") { |
| ok(0, "$path has templates but no filterexceptions.pl file. --ERROR"); |
| next; |
| } |
| else { |
| do "filterexceptions.pl"; |
| if ($^O eq 'MSWin32') { |
| # 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; |
| if ($file ne $orig_file) { |
| $safe{$file} = $safe{$orig_file}; |
| delete $safe{$orig_file}; |
| } |
| } |
| } |
| } |
| |
| # 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) { |
| my $list = $safe{$file}; |
| $safe{$file} = {}; |
| foreach my $directive (@$list) { |
| $safe{$file}{$directive} = 0; |
| } |
| } |
| |
| 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. |
| if ($file =~ /\.(txt|png)\.tmpl$/) { |
| ok(1, "($lang/$flavor) $file is filter-safe"); |
| 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 |
| while ($slurp =~ /\[%(.*?)%\]/gs) { |
| my $directive = $1; |
| |
| my @lineno = ($` =~ m/\n/gs); |
| my $lineno = scalar(@lineno) + 1; |
| |
| if (!directive_ok($file, $directive)) { |
| |
| # 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"); |
| } |
| } |
| |
| my $fullpath = File::Spec->catfile($path, $file); |
| |
| if (@unfiltered) { |
| my $uflist = join("\n ", @unfiltered); |
| ok(0, "($lang/$flavor) $fullpath has unfiltered directives:\n $uflist\n--ERROR"); |
| } |
| 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); |
| ok(0, "($lang/$flavor) $fullpath - filterexceptions.pl has extra members:\n $nflist\n" . |
| "--WARNING"); |
| } |
| else { |
| # Don't use the full path here - it's too long and unwieldy. |
| ok(1, "($lang/$flavor) $file is filter-safe"); |
| } |
| } |
| } |
| } |
| |
| sub directive_ok { |
| my ($file, $directive) = @_; |
| |
| # Comments |
| return 1 if $directive =~ /^[+-]?#/; |
| |
| # Remove any leading/trailing + or - and whitespace. |
| $directive =~ s/^[+-]?\s*//; |
| $directive =~ s/\s*[+-]?$//; |
| |
| # Empty directives are ok; they are usually line break helpers |
| return 1 if $directive eq ''; |
| |
| # 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| |
| BLOCK|USE|ELSE|NEXT|LAST|DEFAULT|FLUSH| |
| ELSIF|SET|SWITCH|CASE|WHILE)/x; |
| |
| # ? : |
| 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 |
| return 1 if $directive =~ /^[\w\.\$]+\s+=\s+/; |
| |
| # 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\(/; |
| |
| # Hooks |
| return 1 if $directive =~ /^Hook.process\(/; |
| |
| # Other functions guaranteed to return OK output |
| return 1 if $directive =~ /^(time2str|GetBugLink|url)\(/; |
| |
| # Safe Template Toolkit virtual methods |
| return 1 if $directive =~ /\.(length$|size$|push\()/; |
| |
| # 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. |
| return 1 if $directive =~ /FILTER\ (html|csv|js|url_quote|css_class_quote| |
| ics|quoteUrls|time|uri|xml|lower| |
| obsolete|inactive|closed|unitconvert| |
| none)\b/x; |
| |
| return 0; |
| } |
| |
| $/ = $oldrecsep; |
| |
| exit 0; |