File Coverage

blib/lib/Template/Directive/XSSAudit.pm
Criterion Covered Total %
statement 95 95 100.0
branch 32 34 94.1
condition 4 4 100.0
subroutine 16 16 100.0
pod 3 7 42.8
total 150 156 96.1


line stmt bran cond sub pod time code
1             package Template::Directive::XSSAudit;
2 7     7   217571 use strict;
  7         17  
  7         265  
3 7     7   35 use warnings;
  7         14  
  7         216  
4 7     7   33 use base qw/ Template::Directive /;
  7         17  
  7         10497  
5              
6             BEGIN {
7 7     7   135620 use vars qw ($VERSION);
  7         22  
  7         13397  
8 7     7   7208 $VERSION = '1.03';
9             }
10              
11             our $DEFAULT_ERROR_HANDLER = sub {
12              
13             my $context = shift || {};
14              
15             warn __PACKAGE__->event_parameter_to_string(
16             $context,
17             'on_error'
18             ) . "\n";
19              
20             };
21             our $DEFAULT_OK_HANDLER = sub { };
22              
23             our @DEFAULT_GOOD_FILTERS = qw( html uri );
24              
25             my $_error_handler = $DEFAULT_ERROR_HANDLER;
26             my $_ok_handler = $DEFAULT_OK_HANDLER;
27             my @_good_filters = @DEFAULT_GOOD_FILTERS;
28              
29              
30             my $_line_info = '';
31             my @checking_get = ();
32             my @applied_filters = ();
33             my $latest_ident = '';
34              
35             =head1 NAME
36              
37             Template::Directive::XSSAudit - TT2 output filtering lint testing
38              
39             =head1 SYNOPSIS
40              
41             use Template;
42             use Template::Directive::XSSAudit;
43              
44             my $tt = Template->new({
45             FACTORY => "Template::Directive::XSSAudit"
46             });
47              
48             my $input = <<'END';
49             Hello [% exploitable.goodness %] World!
50             How would you like to [% play.it.safe | html %] today?
51             END
52              
53             my $out = '';
54             $tt->process(\$input, {}, \$out) || die $tt->error();
55              
56             # output on STDOUT... via default on_error handler
57             # see the documentation of on_error for explanation of the
58             # output format of the default error handler
59             #input file NO_FILTERS line:1 exploitable.goodness
60              
61             =head1 DESCRIPTION
62              
63             This module will help you perform basic lint tests of your template toolkit
64             files.
65              
66             It is intended to parse through all GET items, and make sure that at least
67             one "good" filter is used to escape it.
68              
69             A callback may be provided so that the errors may be handled in a way that
70             makes sense for the project at hand. See C for more details.
71              
72             There is another callback which can be provided named C. This is
73             triggered when a variable is successfully filtered. By default there is
74             no implementation. See C for more details.
75              
76             Additionally, a list of filter names may be provided, instructing the module
77             to require that certain filters be used for output escaping in the tests.
78              
79             Have a look at the t/*.t files that come with the distribution as they
80             leverage the C callback routine.
81              
82             =head1 IMPORTANT NOTES ON SECURITY
83              
84             This tool is NOT a substitude for code and security reviews as it is NOT
85             context aware. This means if you use a html filter in javascript context
86             or css context or html attribute context, you would not be escaping things
87             properly and this tool would not catch that.
88              
89             You also need to make sure that your "good" filters are actually doing their
90             job and escaping things properly.
91              
92             All of this to say, don't let this give you a false sense of security.
93              
94             =head1 EXPORTS
95              
96             None.
97              
98             =head1 METHODS
99              
100             =over 4
101              
102             =item Template::Directive::XSSAudit->on_error ( [ coderef ] )
103              
104             This method is called on every variable involved in a template
105             toolkit 'GET' which was not filtered properly.
106              
107             The callback will be executed in one of two cases:
108              
109             - The variable in question has NO output filtering
110             - The variable is filtered but none of the filters
111             were found in the C list.
112              
113             A default implementation is provided which will simply C any
114             problems which are found with the following information (tab delimited):
115              
116             line:
117              
118             If you call this method without a subroutine reference, it will simply
119             return you the current implementation.
120              
121             If you provide your own callback, it will be passed one parameter
122             which is a hash reference containing the following keys.
123              
124             =over 4
125              
126             =item variable_name
127              
128             This is a string represending the variable name which was found to be
129             incorrectly escaped.
130              
131             =item filtered_by
132              
133             This will contain an array reference containing the names of the filters which
134             were applied to the variable name.
135              
136             If there are entries in this list, it means that no filter in the
137             good filter list was found to apply to the variable. See C for
138             more information.
139              
140             In the case of variables with no filters, this will be an empty array
141             reference.
142              
143             =item file_name
144              
145             The line number in the template file where the problem occurred.
146              
147             This is parsed out as best as can be done but it may come back as an empty
148             string in many cases. It is a convenience item and should not be relied on
149             for any sort of automation.
150              
151             =item file_line
152              
153             The line number in the template file where the problem occurred.
154              
155             This is parsed out as best as can be done but it may come back as an empty
156             string in many cases. It is a convenience item and should not be relied on
157             for any sort of automation.
158              
159             =back
160              
161             =back
162              
163             =cut
164              
165             sub on_error {
166 19     19 1 32189 my $class = shift;
167 19         34 my ($callback) = @_;
168 19 100       59 if( $callback ) {
169 8 100       31 if( ref($callback) ne "CODE" ) {
170 1         19 croak("argument to on_error must be a subroutine reference");
171             }
172 7         14 $_error_handler = $callback;
173             }
174 18         52 return $_error_handler;
175             }
176              
177             =over 4
178              
179             =item Template::Directive::XSSAudit->on_filtered ( [ coderef ] )
180              
181             This method is called on every variable involved in a template
182             toolkit 'GET' which was filtered satisfactorily.
183              
184             By default, no implementation is given so if you want this to
185             do anything, you'll have to provide a coderef yourself.
186              
187             The callback and function works just like C so see the
188             documentation for that method for more details.
189              
190             =back
191              
192             =cut
193              
194             sub on_filtered {
195 16     16 1 56820 my $class = shift;
196 16         29 my ($callback) = @_;
197 16 100       45 if( $callback ) {
198 7 100       1088 if( ref($callback) ne "CODE" ) {
199 1         27 croak("argument to on_filtered must be a subroutine reference");
200             }
201 6         15 $_ok_handler = $callback;
202             }
203 15         48 return $_ok_handler;
204            
205             }
206              
207             sub event_parameter_to_string {
208 7     7 0 3637 my $class = shift;
209 7         12 my($context, $event) = @_;
210              
211 7         12 my $var_name = $context->{variable_name};
212 7         10 my $filters = $context->{filtered_by};
213 7   100     24 my $file = $context->{file_name} || '';
214 7   100     23 my $line_no = $context->{file_line} || 0;
215              
216 7 100       24 my $problem_type = $event eq "on_filtered" ? "OK"
    100          
217             : @$filters ? "NO_SAFE_FILTER"
218             : "NO_FILTERS";
219 7 100       21 my $used_filters = @$filters ? "\t" . (join ",", @$filters) : "";
220              
221 7         39 return sprintf("%s\t%s\tline:%d\t%s%s",
222             $file, $problem_type, $line_no, $var_name, $used_filters
223             )
224             }
225              
226             =over 4
227              
228             =item Template::Directive::XSSAudit->good_filters ( [ arrayref ] )
229              
230             This method will return the current list of "good" filters to you
231             as an array reference. eg.
232              
233             [ 'html', 'uri' ]
234              
235             If you pass an array reference of strings, it will also set the list of good
236             filters. The defaults are simply 'html' and 'uri' but I will be adding more
237             int the future.
238              
239             =back
240              
241             =cut
242              
243             sub good_filters {
244 18     18 1 67712 my $class = shift;
245 18         34 my ($array_ref) = @_;
246 18 100       65 if($array_ref) {
247 6 100       29 if( ref($array_ref) ne "ARRAY" ) {
248 1         19 croak("argument to good_filters must be an array reference");
249             }
250 5         22 @_good_filters = @$array_ref;
251             }
252 17         56 return \@_good_filters;
253             }
254              
255             # ================================================
256             # ========= Template::Directive overrides ========
257             # ================================================
258              
259             sub get {
260 20     20 0 2704 my $class = shift;
261              
262 20         104 my $result = $class->SUPER::get(@_);
263 20         150 $_line_info = _parse_line_info($result);
264 20         60 _trigger_warnings();
265              
266 20 100       90 if( $_[0] =~ /stash->get/ ) {
267 14         33 @checking_get = @_;
268             }
269              
270 20         78 return $result;
271             }
272              
273             sub filter {
274 9     9 0 2592 my $class = shift;
275 9 50       33 if( @checking_get ) {
276 9         89 (my $filter = $_[0][0][0]) =~ s/'//g;
277 9         23 push @applied_filters, $filter
278             }
279              
280 9         62 my $result = $class->SUPER::filter(@_);
281 9         207 $_line_info = _parse_line_info($result);
282 9         30 return $result;
283             }
284              
285              
286             sub ident {
287 21     21 0 150683 my $class = shift;
288 21 50       97 if(!@checking_get) {
289             # TODO: recursive pattern matching on perl expressions
290             # of the form:
291             # $stash->get([ date, 0, format, 0 [ $stash->get([date, 0, now, 0]), %Y/%m/%d ]
292             # take a look at this for inspiration:
293             # http://perldoc.perl.org/perlfaq6.html#Can-I-use-Perl-regular-expressions-to-match-balanced-text%3F
294 21         74 $latest_ident = join '.', grep { "$_" ne "0" } @{$_[0]};
  68         196  
  21         76  
295 21         102 $latest_ident =~ s/'//g;
296             }
297 21         130 my $result = $class->SUPER::ident(@_);
298 21         398 $_line_info = _parse_line_info($result);
299 21         76 return $result;
300             }
301              
302             # 'block' is notably and intentionally missing
303             my @TRIGGER_END_OF_GET_SUBS = qw/
304             anon_block textblock text quoted assign
305             filenames call set default insert include process if
306             foreach next wrapper multi_wrapper while switch try
307             throw clear break return stop use view perl no_perl rawperl
308             capture macro debug template /;
309             for my $method (@TRIGGER_END_OF_GET_SUBS) {
310 7     7   62 no strict;
  7         13  
  7         2941  
311             *$method = sub {
312 25     25   26486 my $class = shift;
313 25         36 my $result = &{"Template::Directive::$method"}("Template::Directive",@_);
  25         156  
314 25         444 $_line_info = _parse_line_info($result);
315 25         54 _trigger_warnings();
316 25         80 return $result;
317             }
318             }
319              
320             ## INTERNAL
321             sub _parse_line_info {
322 75     75   107 my $text = shift;
323 75 100       398 if( $text =~ /^(#line.*)$/m ) {
324 19         74 return $1;
325             }
326 56         206 return $_line_info;
327             }
328              
329             sub _trigger_warnings {
330 45 100   45   186 if( @checking_get ) {
331 14         23 my @good_filters;
332 14 100       40 if(@applied_filters) {
333 9         13 my (%union, %isect);
334 9         14 foreach my $e (@applied_filters, @{good_filters()}) {
  9         29  
335 27 100       107 $union{$e}++ && $isect{$e}++
336             }
337 9         44 @good_filters = keys %isect;
338              
339             }
340              
341 14         86 $_line_info =~ /#line\s+(\d+)\s+"(.+?)"/;
342 14         45 my($file_line,$file_name) = ($1, $2);
343 14 100       63 my $event_sub = !@good_filters ? on_error() : on_filtered();
344 14         124 $event_sub->({
345             variable_name => $latest_ident,
346             filtered_by => [ @applied_filters ],
347             file_name => $file_name,
348             file_line => $file_line,
349             });
350              
351 14         66 $_line_info = '';
352 14         23 $latest_ident = '';
353 14         24 @applied_filters = ();
354 14         96 @checking_get = ();
355             }
356             }
357              
358              
359             1;
360             __END__