Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 167
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
vp_get_real_file_path
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
56
vp_is_interesting_file
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
split_file_to_php_html
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
split_to_php_html
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
182
add_text_to_parsed
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
vp_scan_file
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 1
1260
VP_FileScan
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 3
506
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 get_files
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 _scan_files
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
306
1<?php
2// don't call the file directly
3defined( 'ABSPATH' ) || die( 0 );
4
5class VP_FileScan {
6    var $path;
7    var $last_dir = null;
8    var $offset = 0;
9    var $ignore_symlinks = false;
10
11    function __construct( $path, $ignore_symlinks = false ) {
12        if ( is_dir( $path ) )
13            $this->last_dir = $this->path = @realpath( $path );
14        else
15            $this->last_dir = $this->path = dirname( @realpath( $path ) );
16        $this->ignore_symlinks = $ignore_symlinks;
17    }
18
19    function get_files( $limit = 100 ) {
20        $files = array();
21        if ( is_dir( $this->last_dir ) ) {
22            $return = $this->_scan_files( $this->path, $files, $this->offset, $limit, $this->last_dir );
23            $this->offset = $return[0];
24            $this->last_dir = $return[1];
25            if ( count( $files ) < $limit )
26                $this->last_dir = false;
27        }
28        return $files;
29    }
30
31    function _scan_files( $path, &$files, $offset, $limit, &$last_dir ) {
32        $_offset = 0;
33        if ( is_readable( $path ) && $handle = opendir( $path ) ) {
34            while( false !== ( $entry = readdir( $handle ) ) ) {
35                if ( '.' == $entry || '..' == $entry )
36                    continue;
37
38                ++$_offset;
39                $full_entry = $path . DIRECTORY_SEPARATOR . $entry;
40                $next_item = ltrim( str_replace( $path, '', $last_dir ), DIRECTORY_SEPARATOR );
41                $next = preg_split( '#(?<!\\\\)' . preg_quote( DIRECTORY_SEPARATOR, '#' ) . '#', $next_item, 2 );
42
43                // Skip if the next item is not found.
44                if ( !empty( $next[0] ) && $next[0] != $entry )
45                    continue;
46                if ( rtrim( $last_dir, DIRECTORY_SEPARATOR ) == rtrim( $path, DIRECTORY_SEPARATOR ) && $_offset < $offset )
47                    continue;
48                if ( $this->ignore_symlinks && is_link( $full_entry ) )
49                    continue;
50
51                if ( rtrim( $last_dir, DIRECTORY_SEPARATOR ) == rtrim( $path, DIRECTORY_SEPARATOR ) ) {
52                    // Reset last_dir and offset when we reached the previous last_dir value.
53                    $last_dir = '';
54                    $offset = 0;
55                }
56
57                if ( is_file( $full_entry ) ) {
58                    if ( !vp_is_interesting_file( $full_entry ) )
59                        continue;
60                    $_return_offset = $_offset;
61                    $_return_dir = dirname( $full_entry );
62                    $files[] = $full_entry;
63                } elseif ( is_dir( $full_entry ) ) {
64                    list( $_return_offset, $_return_dir ) = $this->_scan_files( $full_entry, $files, $offset, $limit, $last_dir );
65                }
66                if ( count( $files ) >= $limit ) {
67                    closedir( $handle );
68                    return array( $_return_offset ?? 0, $_return_dir ?? null );
69                }
70            }
71            closedir( $handle );
72        }
73        return array( $_offset, $path );
74    }
75}
76
77function vp_get_real_file_path( $file_path, $tmp_file = false ) {
78    global $site, $site_id;
79    $site_id = !empty( $site->id ) ? $site->id : $site_id;
80    if ( !$tmp_file && !empty( $site_id ) && function_exists( 'determine_file_type_path' ) ) {
81        $path = determine_file_type_path( $file_path );
82        $file = file_by_path( $site_id, $path );
83        if ( !$file )
84            return false;
85        return $file->get_unencrypted();
86    }
87    return !empty( $tmp_file ) ? $tmp_file : $file_path;
88}
89
90function vp_is_interesting_file($file) {
91    $scan_only_regex = apply_filters( 'scan_only_extension_regex', '#\.(ph(p3|p4|p5|p|tml)|html|js|htaccess)$#i' );
92    return preg_match( $scan_only_regex, $file );
93}
94
95/**
96 * Uses the PHP tokenizer to split a file into 3 arrays: PHP code with no comments,
97 * PHP code with comments, and HTML/JS code. Helper wrapper around split_to_php_html()
98 *
99 * @param string $file The file path to read and parse
100 * @return array An array with 3 arrays of lines
101 */
102function split_file_to_php_html( $file ) {
103    $source = @file_get_contents( $file );
104    if ( $source === false ) {
105        $source = '';
106    }
107    return split_to_php_html( $source );
108}
109
110/**
111 * Uses the PHP tokenizer to split a string into 3 arrays: PHP code with no comments,
112 * PHP code with comments, and HTML/JS code.
113 *
114 * @param string $source The file path to read and parse.
115 * @return array An array with 3 arrays of lines
116 */
117function split_to_php_html( $source ) {
118    $tokens = @token_get_all( $source );
119
120    $ret = array( 'php' => array(), 'php-with-comments' => array(), 'html' => array() );
121    $current_line = 0;
122    $mode = 'html'; // need to see an open tag to switch to PHP mode
123
124    foreach ( $tokens as $token ) {
125        if ( ! is_array( $token ) ) {
126            // single character, can't switch our mode; just add it and continue
127            // if it's PHP, should go into both versions; mode 'php' will do that
128            add_text_to_parsed( $ret, $mode, $current_line, $token );
129            $current_line += substr_count( $token, "\n" );
130        } else {
131            // more complex tokens is the interesting case
132            list( $id, $text, $line ) = $token;
133            
134            if ( 'php' === $mode ) {
135                // we're in PHP code
136
137                // might be a comment
138                if ( T_COMMENT === $id || T_DOC_COMMENT === $id ) {
139                    // add it to the PHP with comments array only
140                    add_text_to_parsed( $ret, 'php-with-comments', $current_line, $text );
141
142                    // special case for lines like: "     // comment\n":
143                    // if we're adding a comment with a newline, and the 'php' array current line
144                    // has no trailing newline, add one
145                    if ( substr_count( $text, "\n" ) >= 1 && ! empty( $ret['php'][ $current_line ] ) && 0 === substr_count( $ret['php'][ $current_line ], "\n" ) ) {
146                        $ret['php'][ $current_line ] .= "\n";
147                    }
148
149                    // make sure to count newlines in comments 
150                    $current_line += substr_count( $text, "\n" );
151                    continue;
152                }
153
154                // otherwise add it to both the PHP array and the with comments array
155                add_text_to_parsed( $ret, $mode, $current_line, $text );
156
157                // then see if we're breaking out
158                if ( T_CLOSE_TAG === $id ) {
159                    $mode = 'html';
160                }
161            } else if ( 'html' === $mode ) {
162                // we're in HTML code
163
164                // if we see an open tag, switch to PHP
165                if ( T_OPEN_TAG === $id || T_OPEN_TAG_WITH_ECHO === $id ) {
166                    $mode = 'php';
167                }
168
169                // add to the HTML array (or PHP if it was an open tag)
170                // if it is PHP, this will add it to both arrays, which is what we want
171                add_text_to_parsed( $ret, $mode, $current_line, $text );
172            }
173            $current_line += substr_count( $text, "\n" );
174        }
175    }
176
177    return $ret;
178}
179
180/**
181 * Helper function for split_file_to_php_html; adds a chunk of text to the arrays we'll return.
182 *
183 * @param array  $parsed The array containing all the languages we'll return.
184 * @param string $prefix The prefix for the languages we want to add this text to.
185 * @param int    $start_line_number The line number that this text goes on.
186 * @param string $all_text The text to add.
187 */
188function add_text_to_parsed( &$parsed, $prefix, $start_line_number, $all_text ) {
189    $line_number = $start_line_number;
190
191    // whitespace tokens may span multiple lines; we need to split them up so that the indentation goes on the next line
192    $fragments = explode( "\n", $all_text );
193    foreach ( $fragments as $i => $fragment ) {
194        // each line needs to end with a newline to match the behavior of file()
195        if ( $i < count( $fragments ) - 1 ) {
196            $text = $fragment . "\n";
197        } else {
198            $text = $fragment;
199        }
200
201        if ( '' === $text ) {
202            // check for the empty string explicitly, rather than using empty()
203            // otherwise things like a '0' token will get skipped, because PHP is stupid
204            continue;
205        }
206
207        if ( ! isset( $parsed[ $prefix ][ $line_number ] ) ) {
208            $parsed[ $prefix ][ $line_number ] = '';
209        }
210        $parsed[ $prefix ][ $line_number ] .= $text;
211        if ( 'php' == $prefix ) {
212            if ( ! isset( $parsed[ 'php-with-comments' ][ $line_number ] ) ) {
213                $parsed[ 'php-with-comments' ][ $line_number ] = '';
214            }
215            $parsed[ 'php-with-comments' ][ $line_number ] .= $text;
216        }
217
218        // the caller will also update their line number based on the number of \n characters in the text
219        ++$line_number;
220    }
221}
222/**
223 * Scans a file with the registered signatures. To report a security notice for a specified signature, all its regular
224 * expressions should result in a match.
225 * @param $file the filename to be scanned.
226 * @param null $tmp_file used if the file to be scanned doesn't exist or if the filename doesn't match vp_is_interesting_file().
227 * @return array|bool false if no matched signature is found. A list of matched signatures otherwise.
228 */
229function vp_scan_file( $file, $tmp_file = null, $use_parser = false ) {
230    $real_file = vp_get_real_file_path( $file, $tmp_file );
231    $file_size = file_exists( $real_file ) ? @filesize( $real_file ) : 0;
232    if ( !is_readable( $real_file ) || !$file_size || $file_size > apply_filters( 'scan_max_file_size', 3 * 1024 * 1024 ) ) { // don't scan empty or files larger than 3MB.
233        return false;
234    }
235
236    $file_content = null;
237    $file_parsed = null;
238    $skip_file = apply_filters_ref_array( 'pre_scan_file', array ( false, $file, $real_file, &$file_content ) );
239    if ( false !== $skip_file ) { // maybe detect malware without regular expressions.
240        return $skip_file;
241    }
242
243    if ( !vp_is_interesting_file( $file ) ) { // only scan relevant files.
244        return false;
245    }
246
247    if ( !isset( $GLOBALS['vp_signatures'] ) ) {
248        $GLOBALS['vp_signatures'] = array();
249    }
250
251    $found = array ();
252    foreach ( $GLOBALS['vp_signatures'] as $signature ) {
253        if ( !is_object( $signature ) || !isset( $signature->patterns ) ) {
254            continue;
255        }
256        // if there is no filename_regex, we assume it's the same of vp_is_interesting_file().
257        if ( empty( $signature->filename_regex ) || preg_match( '#' . addcslashes( $signature->filename_regex, '#' ) . '#i', $file ) ) {
258            if ( null === $file_content || !is_array( $file_content ) ) {
259                $file_content = @file( $real_file );
260
261                if ( $file_content === false ) {
262                    return false;
263                }
264
265                if ( $use_parser ) {
266                    $file_parsed = split_file_to_php_html( $real_file );
267                }
268            }
269
270            $is_vulnerable = true;
271
272            $code = $file_content;
273
274            if ( $use_parser ) {
275                // use the language specified in the signature if it has one
276                if ( ! empty( $signature->target_language ) && array_key_exists( $signature->target_language, $file_parsed ) ) {
277                    $code = $file_parsed[ $signature->target_language ];
278
279
280                }
281            }
282
283            $matches = array();
284            if ( ! empty( $signature->patterns ) ) {
285                foreach ( $signature->patterns as $pattern ) {
286                    $match = preg_grep( '#' . addcslashes( $pattern, '#' ) . '#im', $code );
287                    if ( empty( $match ) ) {
288                        $is_vulnerable = false;
289                        break;
290                    }
291
292                    $matches += $match;
293                }
294            }
295
296            // convert the matched line to an array of details showing context around the lines
297            $lines = array();
298
299            $lines_parsed = array();
300
301            $line_indices_parsed = array();
302
303            if ( $use_parser ) {
304                $line_indices_parsed = array_keys( $code );
305            }
306
307            foreach ( $matches as $line => $text ) {
308                $lines = array_merge( $lines, range( $line - 1, $line + 1 ) );
309                if ( $use_parser ) {
310                    $idx = array_search( $line, $line_indices_parsed );
311
312                    // we might be looking at the first or last line; for the non-parsed case, array_intersect_key
313                    // handles this transparently below; for the parsed case, since we have another layer of
314                    // indirection, we have to handle that case here
315                    $idx_around = array();
316                    if ( isset( $line_indices_parsed[ $idx - 1 ] ) ) {
317                        $idx_around[] = $line_indices_parsed[ $idx - 1 ];
318                    }
319                    $idx_around[] = $line_indices_parsed[ $idx ];
320                    if ( isset( $line_indices_parsed[ $idx + 1 ] ) ) {
321                        $idx_around[] = $line_indices_parsed[ $idx + 1 ];
322                    }
323                    $lines_parsed = array_merge( $lines_parsed, $idx_around );
324                }
325            }
326
327            $details = array_intersect_key( $file_content, array_flip( $lines ) );
328
329            $details_parsed = array();
330
331            if ( $use_parser ) {
332                $details_parsed = array_intersect_key( $code, array_flip( $lines_parsed ) );
333            }
334
335            // provide both 'matches' and 'details', as some places want 'matches'
336            // this matches the old behavior, which would add 'details' to some items, without replacing 'matches'
337            $debug_data = array( 'matches' => $matches, 'details' => $details  );
338            if ( $use_parser ) {
339                $debug_data['details_parsed'] = $details_parsed;
340            }
341
342            // Additional checking needed?
343            if ( method_exists( $signature, 'get_detailed_scanner' ) && $scanner = $signature->get_detailed_scanner() )
344                $is_vulnerable = $scanner->scan( $is_vulnerable, $file, $real_file, $file_content, $debug_data );
345            if ( $is_vulnerable ) {
346                $found[$signature->id] = $debug_data;
347                if ( isset( $signature->severity ) && $signature->severity > 8 ) // don't continue scanning
348                    break;
349            }
350        }
351    }
352
353    return apply_filters_ref_array( 'post_scan_file', array ( $found, $file, $real_file, &$file_content ) );
354}