root/trunk/trax/vendor/trax/input_filter.php

Revision 245, 21.1 KB (checked in by john, 5 years ago)

made it so input filter can specify field exceptions not to check

  • Property svn:keywords set to Id
Line 
1<?php
2/**
3 *  File containing the InputFilter class
4 *
5 *  (PHP 5)
6 *
7 *  @package PHPonTrax
8 *  @version $Id$
9 *  @author Daniel Morris
10 *  contributors: Gianpaolo Racca, Ghislain Picard, Marco Wandschneider,
11 *                Chris Tobin and Andrew Eddie.
12 *  @copyright Daniel Morris <dan@rootcube.com>
13 *  @license http://opensource.org/licenses/gpl-license.php GNU Public License
14 */
15
16/**
17 *  Filter user input to remove potential security threats
18 *
19 *  InputFilter has three public methods that are useful in protecting
20 *  a web site from potential security threats from user input.
21 *  <ul>
22 *    <li>{@link safeSQL()} protects SQL from the user.</li>
23 *    <li>{@link process()} protects HTML tags and attributes from the
24 *      user.</li>
25 *    <li>{@link process_all()} applies {@link process()} to all
26 *      possible sources of user input</li>
27 *  </ul>
28 *  For usage instructions see
29 *  {@tutorial PHPonTrax/InputFilter.cls the class tutorial}.
30 *  @todo Check FIXMEs
31 */
32class InputFilter {
33   
34    /**
35     *  User-provided list of tags to either accept or reject
36     *
37     *  Whether the tags in this list are accepted or rejected is
38     *  determined by the value of {@link $tagsMethod}.
39     *  @var string[]
40     */
41    protected static $tagsArray = array();  // default = empty array
42   
43    /**
44     *  User-provided list of attributes to either accept or reject
45     *
46     *  Whether the attributes in this list are accepted or rejected is
47     *  determined by the value of {@link $attrMethod}.
48     *  @var string[]
49     */
50    protected static $attrArray = array();  // default = empty array
51   
52    /**
53     *  How to apply user-provided tags list
54     *
55     *  Which method to use when applying the list of tags provided by
56     *  the user and stored in {@link $tagsArray}.
57     *  @var boolean Tested by {@link filterTags()} to see whether the
58     *               user-provide list of tags in {@link $tagsArray}
59     *               describes those tags which are forbidden, or
60     *               those tags which are permitted.  Default false.
61     *  <ul>
62     *    <li>true =>  Remove  those tags which are in
63     *                 {@link $tagsArray}.</li>
64     *    <li>false => Allow only those tags which are listed in
65     *                 {@link $tagsArray}.</li>
66     *  </ul>
67     */
68    protected static $tagsMethod = true;
69   
70    /**
71     *  How to apply user-provided attribute list
72     *
73     *  Which method to use when applying the list of attributes
74     *  provided by the user and stored in {@link $attrArray}.
75     *  @var boolean Tested by {@link filterAttr()} to see whether the
76     *               user-provide list of tags in {@link $attrArray}
77     *               describes those tags which are forbidden, or
78     *               those tags which are permitted.  Default false.
79     *  <ul>
80     *    <li>true =>  Remove  those tags which are in
81     *                 {@link $attrArray}.</li>
82     *    <li>false => Allow only those tags which are listed in
83     *                 {@link $attrArray}.</li>
84     *  </ul>
85     */
86    protected static $attrMethod = true;
87
88   
89    /**
90     *  Whether to remove blacklisted tags and attributes
91     *
92     *  @var boolean Tested by {@link filterAttr()} and
93     *               {@link filterTags()} to see whether to remove
94     *               blacklisted tags and attributes.  Default true.
95     *  <ul>
96     *    <li>true => Remove tags in {@link $tagBlacklist} and
97     *                attributes in {@link $attrBlacklist}, in
98     *                addition to all other potentially suspect tags
99     *                and attributes.</li>
100     *    <li>false => Remove potentially suspect tags and attributes
101     *      without consulting{@link $tagBlacklist} or
102     *      {@link $attrBlacklist}.</li>
103     *  </ul>
104     */
105    protected static $xssAuto = true;
106
107    /**
108     *  Fields to ignore that you want html and other banned stuff in.
109     *
110     *  @var array
111     */ 
112    protected static $exception_fields = array();
113   
114    /**
115     *  List of tags to be removed
116     *
117     *  If {@link $xssAuto} is true, remove the tags in this list.
118     *  @var string[]
119     */
120    protected static $tagBlacklist =
121        array('applet', 'body', 'bgsound', 'base', 'basefont', 'embed',
122              'frame', 'frameset', 'head', 'html', 'id', 'iframe',
123              'ilayer', 'layer', 'link', 'meta', 'name', 'object',
124              'script', 'style', 'title', 'xml');
125   
126    /**
127     *  List of attributes to be removed
128     *
129     *  If {@link $xssAuto} is true, remove the attributes in this list.
130     *  @var string[]
131     */
132    protected static $attrBlacklist =
133        array('action', 'background', 'codebase', 'dynsrc', 'lowsrc'); 
134       
135    /**
136     *  Initializer for InputFilter class.
137     *
138     *  @param string[] $tagsArray  User-provided list of tags to
139     *                              either accept or reject.  Default: none
140     *  @param string[] $attrArray  User-provided list of attributes to
141     *                              either accept or reject.  Default: none
142     *  @param boolean $tagsMethod How to apply the list of tags in $tagsArray:
143     *  <ul>
144     *    <li>true =>  Remove  those tags which are listed in
145     *                 $tagsArray.</li> 
146     *    <li>false => Allow only those tags which are listed in
147     *                 $tagsArray.</li> 
148     *  </ul>
149     *  Default: false
150     *  @param boolean $attrMethod How to apply the list of attributess in $attrArray:
151     *  <ul>
152     *    <li>true =>  Remove  those attributes which are listed in
153     *                 $attrArray.</li> 
154     *    <li>false => Allow only those attributes which are listed in
155     *                 $attrArray.</li> 
156     *  </ul>
157     *  Default: false
158     *  @param boolean $xssAuto Behavior of {@link filterTags()}:
159     *  <ul>
160     *    <li>true => Remove tags in {@link $tagBlacklist} and
161     *                attributes in {@link $attrBlacklist}, in
162     *                addition to all other potentially suspect tags
163     *                and attributes.</li>
164     *    <li>false => Remove potentially suspect tags and attributes
165     *      without consulting{@link $tagBlacklist} or
166     *      {@link $attrBlacklist}.</li>
167     *  </ul>
168     *  Default: true
169     *  @uses $attrArray
170     *  @uses $attrMethod
171     *  @uses $tagsArray
172     *  @uses $tagsMethod
173     */
174    public function init($tagsArray = array(), $attrArray = array(),
175                                $tagsMethod = true, $attrMethod = true,
176                                $xssAuto = true) { 
177                                   
178        // make sure user defined arrays are in lowercase
179        for ($i = 0; $i < count($tagsArray); $i++) $tagsArray[$i] = strtolower($tagsArray[$i]);
180        for ($i = 0; $i < count($attrArray); $i++) $attrArray[$i] = strtolower($attrArray[$i]);
181        // assign to member vars
182        self::$tagsArray = (array) $tagsArray;
183        self::$attrArray = (array) $attrArray;
184        self::$tagsMethod = $tagsMethod;
185        self::$attrMethod = $attrMethod;
186        self::$xssAuto = $xssAuto;
187    }
188
189    /**
190     *  Adds a field to exclude from filtering
191     *
192     */ 
193    public function add_field_exception($field) {
194        if($field) {
195            self::$exception_fields[] = $field;   
196        }
197    }
198
199    /**
200     *  Clears all previous field exceptions
201     *
202     */     
203    public function clear_field_exceptions() {
204        self::$exception_fields = array();       
205    } 
206
207    /**
208     *  Remove forbidden tags and attributes from user input
209     *
210     *  Construct an InputFilter object.  Then apply the
211     *  {@link process()} method to each of the user input arrays
212     *  {@link http://www.php.net/reserved.variables#reserved.variables.post $_POST},
213     *  {@link http://www.php.net/reserved.variables#reserved.variables.get $_GET} and
214     *  {@link http://www.php.net/reserved.variables#reserved.variables.request $_REQUEST}.
215     *  <b>FIXME:</b> isn't it partly redundant to do this to $_REQUEST?
216     *  Shouldn't we do it to $_COOKIE instead?
217     *  @param string[] $tagsArray  User-provided list of tags to
218     *                              either accept or reject.  Default: none
219     *  @param string[] $attrArray  User-provided list of attributes to
220     *                              either accept or reject.  Default: none
221     *  @param boolean $tagsMethod How to apply the list of tags in $tagsArray:
222     *  <ul>
223     *    <li>true =>  Remove  those tags which are listed in
224     *                 $tagsArray.</li> 
225     *    <li>false => Allow only those tags which are listed in
226     *                 $tagsArray.</li> 
227     *  </ul>
228     *  Default: false
229     *  @param boolean $attrMethod How to apply the list of attributess in $attrArray:
230     *  <ul>
231     *    <li>true =>  Remove  those attributes which are listed in
232     *                 $attrArray.</li> 
233     *    <li>false => Allow only those attributes which are listed in
234     *                 $attrArray.</li> 
235     *  </ul>
236     *  Default: false
237     *  @param boolean $xssAuto Behavior of {@link filterTags()}:
238     *  <ul>
239     *    <li>true => Remove tags in {@link $tagBlacklist} and
240     *                attributes in {@link $attrBlacklist}, in
241     *                addition to all other potentially suspect tags
242     *                and attributes.</li>
243     *    <li>false => Remove potentially suspect tags and attributes
244     *      without consulting{@link $tagBlacklist} or
245     *      {@link $attrBlacklist}.</li>
246     *  </ul>
247     *  Default: true
248     *  @author John Peterson
249     *  @uses __construct()
250     *  @uses process()
251     *  @todo Check out FIXMEs
252     */
253    public function process_all($tagsArray = array(), $attrArray = array(),
254                                $tagsMethod = true, $attrMethod = true,
255                                $xssAuto = true) {
256        self::init($tagsArray, $attrArray, $tagsMethod,
257                          $attrMethod, $xssAuto);
258        if(count($_POST)) {
259            $_POST = self::process($_POST);
260        }
261        if(count($_GET)) {
262            $_GET = self::process($_GET);
263        }
264        if(count($_REQUEST)) {
265            $_REQUEST = self::process($_REQUEST);
266        }
267    }
268   
269    /**
270     *  Remove forbidden tags and attributes from array of strings
271     *
272     *  Accept a string or array of strings.  For each string in the
273     *  source, remove the forbidden tags and attributes from the string.
274     *  @param mixed $source - input string/array-of-string to be 'cleaned'
275     *  @return mixed 'cleaned' version of input parameter
276     *  @uses decode()
277     *  @uses remove()
278     */
279    public function process($source, $extra_key = null) {
280        // clean all elements in this array
281        if(is_array($source)) {
282            foreach($source as $key => $value) {
283                //error_log("key:".$extra_key.$key);
284                if(in_array($extra_key.$key, self::$exception_fields)) { $source[$key] = $value; continue; }
285                // for arrays in arrays
286                if (is_array($value)) $source[$key] = self::process($value, $key.":");
287                // filter element for XSS and other 'bad' code etc.
288                if (is_string($value)) $source[$key] = self::remove(self::decode($value));
289            }
290            return $source;
291        // clean this string
292        } elseif(is_string($source)) {
293            // filter source for XSS and other 'bad' code etc.
294            return self::remove(self::decode($source));
295        // return parameter as given
296        } else {
297            return $source; 
298        }
299    }
300
301    /**
302     *  Remove forbidden tags and attributes from a string iteratively
303     *
304     *  Call {@link filterTags()} repeatedly until no change in the
305     *  input is produced.
306     *  @param string $source Input string to be 'cleaned'
307     *  @return string 'cleaned' version of $source
308     *  @uses filterTags()
309     */
310    protected function remove($source) {
311        // provides nested-tag protection
312        while($source != self::filterTags($source)) {
313            $source = self::filterTags($source);
314        }
315        return $source;
316    }   
317   
318    /**
319     *  Remove forbidden tags and attributes from a string
320     *
321     *  Inspect the input for tags "<tagname ...>" and check the tag
322     *  name against a list of forbidden tag names.  Delete all tags
323     *  with forbidden names.  If {@link $xssAuto} is true, delete all
324     *  tags in {@link $tagBlacklist}.  If there is a user-defined tag
325     *  list in {@link $tagsArray}, process according to the value of
326     *  {@link $tagsMethod}.
327     *
328     *  If the tag name is OK, then call {@link filterAttr()} to check
329     *  all attributes of the tag and delete forbidden attributes.
330     *  @param string $source Input string to be 'cleaned'
331     *  @return string Cleaned version of input parameter
332     *  @uses filterAttr()
333     *  @uses $tagBlacklist
334     *  @uses $tagsArray
335     *  @uses $tagsMethod
336     *  @uses $xssAuto
337     */
338    protected function filterTags($source) {
339        // filter pass setup
340        $preTag = null;
341        $postTag = $source;
342        // find initial tag's position
343        $tagOpen_start = strpos($source, '<');
344        // interate through string until no tags left
345        while($tagOpen_start !== false) {
346            // process tag interatively
347            $preTag .= substr($postTag, 0, $tagOpen_start);
348            $postTag = substr($postTag, $tagOpen_start);
349            $fromTagOpen = substr($postTag, 1);
350            // end of tag
351            $tagOpen_end = strpos($fromTagOpen, '>');
352            if ($tagOpen_end === false) break;
353            // next start of tag (for nested tag assessment)
354            $tagOpen_nested = strpos($fromTagOpen, '<');
355            if (($tagOpen_nested !== false) && ($tagOpen_nested < $tagOpen_end)) {
356                $preTag .= substr($postTag, 0, ($tagOpen_nested+1));
357                $postTag = substr($postTag, ($tagOpen_nested+1));
358                $tagOpen_start = strpos($postTag, '<');
359                continue;
360            } 
361            $tagOpen_nested = (strpos($fromTagOpen, '<') + $tagOpen_start + 1);
362            $currentTag = substr($fromTagOpen, 0, $tagOpen_end);
363            $tagLength = strlen($currentTag);
364            if (!$tagOpen_end) {
365                $preTag .= $postTag;
366                $tagOpen_start = strpos($postTag, '<');         
367            }
368            // iterate through tag finding attribute pairs - setup
369            $tagLeft = $currentTag;
370            $attrSet = array();
371            $currentSpace = strpos($tagLeft, ' ');
372            // is end tag
373            if (substr($currentTag, 0, 1) == "/") {
374                $isCloseTag = true;
375                list($tagName) = explode(' ', $currentTag);
376                $tagName = substr($tagName, 1);
377            // is start tag
378            } else {
379                $isCloseTag = false;
380                list($tagName) = explode(' ', $currentTag);
381            }       
382            // excludes all "non-regular" tagnames OR no tagname OR remove if xssauto is on and tag is blacklisted
383            if ((!preg_match("/^[a-z][a-z0-9]*$/i",$tagName)) || (!$tagName) || ((in_array(strtolower($tagName), self::$tagBlacklist)) && (self::$xssAuto))) {
384                $postTag = substr($postTag, ($tagLength + 2));
385                $tagOpen_start = strpos($postTag, '<');
386                // don't append this tag
387                continue;
388            }
389            // this while is needed to support attribute values with spaces in!
390            while ($currentSpace !== false) {
391                $fromSpace = substr($tagLeft, ($currentSpace+1));
392                $nextSpace = strpos($fromSpace, ' ');
393                $openQuotes = strpos($fromSpace, '"');
394                $closeQuotes = strpos(substr($fromSpace, ($openQuotes+1)), '"') + $openQuotes + 1;
395                // another equals exists
396                if (strpos($fromSpace, '=') !== false) {
397                    // opening and closing quotes exists
398                    if (($openQuotes !== false) && (strpos(substr($fromSpace, ($openQuotes+1)), '"') !== false))
399                        $attr = substr($fromSpace, 0, ($closeQuotes+1));
400                    // one or neither exist
401                    else $attr = substr($fromSpace, 0, $nextSpace);
402                // no more equals exist
403                } else $attr = substr($fromSpace, 0, $nextSpace);
404                // last attr pair
405                if (!$attr) $attr = $fromSpace;
406                // add to attribute pairs array
407                $attrSet[] = $attr;
408                // next inc
409                $tagLeft = substr($fromSpace, strlen($attr));
410                $currentSpace = strpos($tagLeft, ' ');
411            }
412            // appears in array specified by user
413            $tagFound = in_array(strtolower($tagName), self::$tagsArray);
414            // remove this tag on condition
415            if ((!$tagFound && self::$tagsMethod) || ($tagFound && !self::$tagsMethod)) {
416                // reconstruct tag with allowed attributes
417                if (!$isCloseTag) {
418                    $attrSet = self::filterAttr($attrSet);
419                    $preTag .= '<' . $tagName;
420                    for ($i = 0; $i < count($attrSet); $i++)
421                        $preTag .= ' ' . $attrSet[$i];
422                    // reformat single tags to XHTML
423                    if (strpos($fromTagOpen, "</" . $tagName)) $preTag .= '>';
424                    else $preTag .= ' />';
425                // just the tagname
426                } else $preTag .= '</' . $tagName . '>';
427            }
428            // find next tag's start
429            $postTag = substr($postTag, ($tagLength + 2));
430            $tagOpen_start = strpos($postTag, '<');         
431        }
432        // append any code after end of tags
433        $preTag .= $postTag;
434        return $preTag;
435    }
436
437    /**
438     *  Internal method to strip a tag of certain attributes
439     *
440     *  Remove potentially dangerous attributes from a set of
441     *  "attr=value" strings.  Attributes considered dangerous are:
442     *  <ul>
443     *    <li>Any attribute name containing any non-alphabetic
444     *      character</li>
445     *    <li>Any attribute name beginning "on..."</li>
446     *    <li>If {@link $xssAuto} is true, any attribute name in
447     *      {@link $attrBlacklist}</li>
448     *    <li>Any attribute with a value containing the strings
449     *      'javascript:', 'behaviour:', 'vbscript:', 'mocha:',
450     *      'livescript:'</li>
451     *    <li>Any attribute whose name contains 'style' and whose
452     *      value contains 'expression'.</li>
453     *    <li>If there is a user-provided list of attributes in
454     *      {@link $attrArray}, process according to the value of
455     *      {@link $attrMethod}.</li>
456     *  </ul>
457     *  @param string[] $attrSet Array of strings "attr=value" parsed
458     *                           from a tag.
459     *  @return string[] Input with potentially dangerous attributes
460     *                   removed
461     *  @uses $attrArray
462     *  @uses $attrBlacklist
463     *  @uses $attrMethod
464     *  @uses $xssAuto
465     */
466    protected function filterAttr($attrSet) {   
467        $newSet = array();
468        // process attributes
469        for ($i = 0; $i <count($attrSet); $i++) {
470            // skip blank spaces in tag
471            if (!$attrSet[$i]) continue;
472            // split into attr name and value
473            $attrSubSet = explode('=', trim($attrSet[$i]));
474            list($attrSubSet[0]) = explode(' ', $attrSubSet[0]);
475            // removes all "non-regular" attr names AND also attr blacklisted
476            if ((!eregi("^[a-z]*$",$attrSubSet[0])) || ((self::$xssAuto) && ((in_array(strtolower($attrSubSet[0]), self::$attrBlacklist)) || (substr($attrSubSet[0], 0, 2) == 'on'))))
477                continue;
478            // xss attr value filtering
479            if ($attrSubSet[1] || is_numeric($attrSubSet[1])) {
480                // strips unicode, hex, etc
481                $attrSubSet[1] = str_replace('&#', '', $attrSubSet[1]);
482                // strip normal newline within attr value
483                $attrSubSet[1] = preg_replace('/\s+/', '', $attrSubSet[1]);
484                // strip double quotes
485                $attrSubSet[1] = str_replace('"', '', $attrSubSet[1]);
486                // [requested feature] convert single quotes from either side to doubles (Single quotes shouldn't be used to pad attr value)
487                if ((substr($attrSubSet[1], 0, 1) == "'") && (substr($attrSubSet[1], (strlen($attrSubSet[1]) - 1), 1) == "'"))
488                    $attrSubSet[1] = substr($attrSubSet[1], 1, (strlen($attrSubSet[1]) - 2));
489                // strip slashes
490                $attrSubSet[1] = stripslashes($attrSubSet[1]);
491            }
492            // auto strip attr's with "javascript:
493            if (((strpos(strtolower($attrSubSet[1]), 'expression') !== false) && 
494                (strtolower($attrSubSet[0]) == 'style')) ||
495                (strpos(strtolower($attrSubSet[1]), 'javascript:') !== false) ||
496                (strpos(strtolower($attrSubSet[1]), 'behaviour:') !== false) ||
497                (strpos(strtolower($attrSubSet[1]), 'vbscript:') !== false) ||
498                (strpos(strtolower($attrSubSet[1]), 'mocha:') !== false) ||
499                (strpos(strtolower($attrSubSet[1]), 'livescript:') !== false) 
500            ) { continue; }
501
502            // if matches user defined array
503            $attrFound = in_array(strtolower($attrSubSet[0]), self::$attrArray);
504            //error_log("attrFound:".($attrFound ? "Yes" : "No"));
505            // keep this attr on condition
506            if ((!$attrFound && self::$attrMethod) || ($attrFound && !self::$attrMethod)) {
507                //error_log($attrSubSet[0]."=".$attrSubSet[1]);
508                // attr has value
509                if($attrSubSet[1]) {
510                    $newSet[] = $attrSubSet[0] . '="' . $attrSubSet[1] . '"';
511                // attr has decimal zero as value
512                } elseif ($attrSubSet[1] == "0") { 
513                    $newSet[] = $attrSubSet[0] . '="0"';
514                // reformat single attributes to XHTML
515                } else {
516                    $newSet[] = $attrSubSet[0] . '="' . $attrSubSet[0] . '"';
517                }
518            }   
519        }
520        return $newSet;
521    }
522   
523    /**
524     *  Convert HTML entities to characters
525     *
526     *  Convert input string containing HTML entities to the
527     *  corresponding character (&amp; => &).  ISO 8859-1 character
528     *  set is assumed.
529     *  @param string $source Character string containing HTML entities
530     *  @return string Input string, with entities converted to characters
531     *  @uses chr()
532     *  @uses html_entity_decode()
533     *  @uses preg_replace()
534     */
535    protected function decode($source) {
536        // url decode
537        $source = html_entity_decode($source, ENT_QUOTES, "ISO-8859-1");
538        // convert decimal &#DDD; to character DDD
539        $source = preg_replace('/&#(\d+);/me',"chr(\\1)", $source);
540        // convert hex &#xXXX; to character XXX
541        $source = preg_replace('/&#x([a-f0-9]+);/mei',"chr(0x\\1)", $source);
542        return $source;
543    }
544}
545
546// -- set Emacs parameters --
547// Local variables:
548// tab-width: 4
549// c-basic-offset: 4
550// c-hanging-comment-ender-p: nil
551// indent-tabs-mode: nil
552// End:
553?>
Note: See TracBrowser for help on using the browser.