Automattic /
jetpack
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
| 1 | <?php |
||
| 2 | // don't call the file directly |
||
| 3 | defined( 'ABSPATH' ) or die(); |
||
| 4 | |||
| 5 | class 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, $_return_dir ); |
||
|
0 ignored issues
–
show
The variable
$_return_offset does not seem to be defined for all execution paths leading up to this point.
If you define a variable conditionally, it can happen that it is not defined for all execution paths. Let’s take a look at an example: function myFunction($a) {
switch ($a) {
case 'foo':
$x = 1;
break;
case 'bar':
$x = 2;
break;
}
// $x is potentially undefined here.
echo $x;
}
In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined. Available Fixes
Loading history...
|
|||
| 69 | } |
||
| 70 | } |
||
| 71 | closedir( $handle ); |
||
| 72 | } |
||
| 73 | return array( $_offset, $path ); |
||
| 74 | } |
||
| 75 | } |
||
| 76 | |||
| 77 | function 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 | |||
| 90 | function 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 | */ |
||
| 102 | function 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 $file The file path to read and parse |
||
|
0 ignored issues
–
show
There is no parameter named
$file. Was it maybe removed?
This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. Consider the following example. The parameter /**
* @param array $germany
* @param array $island
* @param array $italy
*/
function finale($germany, $island) {
return "2:1";
}
The most likely cause is that the parameter was removed, but the annotation was not. Loading history...
|
|||
| 115 | * @return array An array with 3 arrays of lines |
||
| 116 | */ |
||
| 117 | function 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; |
||
|
0 ignored issues
–
show
The assignment to
$line is unused. Consider omitting it like so list($first,,$third).
This checks looks for assignemnts to variables using the Consider the following code example. <?php
function returnThreeValues() {
return array('a', 'b', 'c');
}
list($a, $b, $c) = returnThreeValues();
print $a . " - " . $c;
Only the variables Instead, the list call could have been. list($a,, $c) = returnThreeValues();
Loading history...
|
|||
| 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 && isset( $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 | * @param array $parsed The array containing all the languages we'll return |
||
| 183 | * @param string $prefix The prefix for the languages we want to add this text to |
||
| 184 | * @param int $line_number The line number that this text goes on |
||
|
0 ignored issues
–
show
There is no parameter named
$line_number. Did you maybe mean $start_line_number?
This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit. Consider the following example. The parameter /**
* @param array $germany
* @param array $ireland
*/
function finale($germany, $island) {
return "2:1";
}
The most likely cause is that the parameter was changed, but the annotation was not. Loading history...
|
|||
| 185 | * @param string $text The text to add |
||
|
0 ignored issues
–
show
There is no parameter named
$text. Was it maybe removed?
This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. Consider the following example. The parameter /**
* @param array $germany
* @param array $island
* @param array $italy
*/
function finale($germany, $island) {
return "2:1";
}
The most likely cause is that the parameter was removed, but the annotation was not. Loading history...
|
|||
| 186 | */ |
||
| 187 | function add_text_to_parsed( &$parsed, $prefix, $start_line_number, $all_text ) { |
||
| 188 | $line_number = $start_line_number; |
||
| 189 | |||
| 190 | // whitespace tokens may span multiple lines; we need to split them up so that the indentation goes on the next line |
||
| 191 | $fragments = explode( "\n", $all_text ); |
||
| 192 | foreach ( $fragments as $i => $fragment ) { |
||
| 193 | // each line needs to end with a newline to match the behavior of file() |
||
| 194 | if ( $i < count( $fragments ) - 1 ) { |
||
| 195 | $text = $fragment . "\n"; |
||
| 196 | } else { |
||
| 197 | $text = $fragment; |
||
| 198 | } |
||
| 199 | |||
| 200 | if ( '' === $text ) { |
||
| 201 | // check for the empty string explicitly, rather than using empty() |
||
| 202 | // otherwise things like a '0' token will get skipped, because PHP is stupid |
||
| 203 | continue; |
||
| 204 | } |
||
| 205 | |||
| 206 | if ( ! isset( $parsed[ $prefix ][ $line_number ] ) ) { |
||
| 207 | $parsed[ $prefix ][ $line_number ] = ''; |
||
| 208 | } |
||
| 209 | $parsed[ $prefix ][ $line_number ] .= $text; |
||
| 210 | if ( 'php' == $prefix ) { |
||
| 211 | if ( ! isset( $parsed[ 'php-with-comments' ][ $line_number ] ) ) { |
||
| 212 | $parsed[ 'php-with-comments' ][ $line_number ] = ''; |
||
| 213 | } |
||
| 214 | $parsed[ 'php-with-comments' ][ $line_number ] .= $text; |
||
| 215 | } |
||
| 216 | |||
| 217 | // the caller will also update their line number based on the number of \n characters in the text |
||
| 218 | $line_number++; |
||
| 219 | } |
||
| 220 | } |
||
| 221 | /** |
||
| 222 | * Scans a file with the registered signatures. To report a security notice for a specified signature, all its regular |
||
| 223 | * expressions should result in a match. |
||
| 224 | * @param $file the filename to be scanned. |
||
| 225 | * @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(). |
||
| 226 | * @return array|bool false if no matched signature is found. A list of matched signatures otherwise. |
||
| 227 | */ |
||
| 228 | function vp_scan_file( $file, $tmp_file = null, $use_parser = false ) { |
||
| 229 | $real_file = vp_get_real_file_path( $file, $tmp_file ); |
||
|
0 ignored issues
–
show
$tmp_file is of type null, but the function expects a boolean.
It seems like the type of the argument is not accepted by the function/method which you are calling. In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug. We suggest to add an explicit type cast like in the following example: function acceptsInteger($int) { }
$x = '123'; // string "123"
// Instead of
acceptsInteger($x);
// we recommend to use
acceptsInteger((integer) $x);
Loading history...
|
|||
| 230 | $file_size = file_exists( $real_file ) ? @filesize( $real_file ) : 0; |
||
| 231 | 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. |
||
| 232 | return false; |
||
| 233 | } |
||
| 234 | |||
| 235 | $file_content = null; |
||
| 236 | $file_parsed = null; |
||
| 237 | $skip_file = apply_filters_ref_array( 'pre_scan_file', array ( false, $file, $real_file, &$file_content ) ); |
||
| 238 | if ( false !== $skip_file ) { // maybe detect malware without regular expressions. |
||
| 239 | return $skip_file; |
||
| 240 | } |
||
| 241 | |||
| 242 | if ( !vp_is_interesting_file( $file ) ) { // only scan relevant files. |
||
| 243 | return false; |
||
| 244 | } |
||
| 245 | |||
| 246 | if ( !isset( $GLOBALS['vp_signatures'] ) ) { |
||
| 247 | $GLOBALS['vp_signatures'] = array(); |
||
| 248 | } |
||
| 249 | |||
| 250 | $found = array (); |
||
| 251 | foreach ( $GLOBALS['vp_signatures'] as $signature ) { |
||
| 252 | if ( !is_object( $signature ) || !isset( $signature->patterns ) ) { |
||
| 253 | continue; |
||
| 254 | } |
||
| 255 | // if there is no filename_regex, we assume it's the same of vp_is_interesting_file(). |
||
| 256 | if ( empty( $signature->filename_regex ) || preg_match( '#' . addcslashes( $signature->filename_regex, '#' ) . '#i', $file ) ) { |
||
| 257 | if ( null === $file_content || !is_array( $file_content ) ) { |
||
| 258 | $file_content = @file( $real_file ); |
||
| 259 | |||
| 260 | if ( $file_content === false ) { |
||
| 261 | return false; |
||
| 262 | } |
||
| 263 | |||
| 264 | if ( $use_parser ) { |
||
| 265 | $file_parsed = split_file_to_php_html( $real_file ); |
||
| 266 | } |
||
| 267 | } |
||
| 268 | |||
| 269 | $is_vulnerable = true; |
||
| 270 | |||
| 271 | $code = $file_content; |
||
| 272 | |||
| 273 | if ( $use_parser ) { |
||
| 274 | // use the language specified in the signature if it has one |
||
| 275 | if ( ! empty( $signature->target_language ) && array_key_exists( $signature->target_language, $file_parsed ) ) { |
||
| 276 | $code = $file_parsed[ $signature->target_language ]; |
||
| 277 | |||
| 278 | |||
| 279 | } |
||
| 280 | } |
||
| 281 | |||
| 282 | $matches = array(); |
||
| 283 | if ( ! empty( $signature->patterns ) ) { |
||
| 284 | foreach ( $signature->patterns as $pattern ) { |
||
| 285 | $match = preg_grep( '#' . addcslashes( $pattern, '#' ) . '#im', $code ); |
||
| 286 | if ( empty( $match ) ) { |
||
| 287 | $is_vulnerable = false; |
||
| 288 | break; |
||
| 289 | } |
||
| 290 | |||
| 291 | $matches += $match; |
||
| 292 | } |
||
| 293 | } |
||
| 294 | |||
| 295 | // convert the matched line to an array of details showing context around the lines |
||
| 296 | $lines = array(); |
||
| 297 | |||
| 298 | $lines_parsed = array(); |
||
| 299 | |||
| 300 | $line_indices_parsed = array(); |
||
| 301 | |||
| 302 | if ( $use_parser ) { |
||
| 303 | $line_indices_parsed = array_keys( $code ); |
||
| 304 | } |
||
| 305 | |||
| 306 | foreach ( $matches as $line => $text ) { |
||
| 307 | $lines = array_merge( $lines, range( $line - 1, $line + 1 ) ); |
||
| 308 | if ( $use_parser ) { |
||
| 309 | $idx = array_search( $line, $line_indices_parsed ); |
||
| 310 | |||
| 311 | // we might be looking at the first or last line; for the non-parsed case, array_intersect_key |
||
| 312 | // handles this transparently below; for the parsed case, since we have another layer of |
||
| 313 | // indirection, we have to handle that case here |
||
| 314 | $idx_around = array(); |
||
| 315 | if ( isset( $line_indices_parsed[ $idx - 1 ] ) ) { |
||
| 316 | $idx_around[] = $line_indices_parsed[ $idx - 1 ]; |
||
| 317 | } |
||
| 318 | $idx_around[] = $line_indices_parsed[ $idx ]; |
||
| 319 | if ( isset( $line_indices_parsed[ $idx + 1 ] ) ) { |
||
| 320 | $idx_around[] = $line_indices_parsed[ $idx + 1 ]; |
||
| 321 | } |
||
| 322 | $lines_parsed = array_merge( $lines_parsed, $idx_around ); |
||
| 323 | } |
||
| 324 | } |
||
| 325 | |||
| 326 | $details = array_intersect_key( $file_content, array_flip( $lines ) ); |
||
| 327 | |||
| 328 | $details_parsed = array(); |
||
| 329 | |||
| 330 | if ( $use_parser ) { |
||
| 331 | $details_parsed = array_intersect_key( $code, array_flip( $lines_parsed ) ); |
||
| 332 | } |
||
| 333 | |||
| 334 | // provide both 'matches' and 'details', as some places want 'matches' |
||
| 335 | // this matches the old behavior, which would add 'details' to some items, without replacing 'matches' |
||
| 336 | $debug_data = array( 'matches' => $matches, 'details' => $details ); |
||
| 337 | if ( $use_parser ) { |
||
| 338 | $debug_data['details_parsed'] = $details_parsed; |
||
| 339 | } |
||
| 340 | |||
| 341 | // Additional checking needed? |
||
| 342 | if ( method_exists( $signature, 'get_detailed_scanner' ) && $scanner = $signature->get_detailed_scanner() ) |
||
| 343 | $is_vulnerable = $scanner->scan( $is_vulnerable, $file, $real_file, $file_content, $debug_data ); |
||
| 344 | if ( $is_vulnerable ) { |
||
| 345 | $found[$signature->id] = $debug_data; |
||
| 346 | if ( isset( $signature->severity ) && $signature->severity > 8 ) // don't continue scanning |
||
| 347 | break; |
||
| 348 | } |
||
| 349 | } |
||
| 350 | } |
||
| 351 | |||
| 352 | return apply_filters_ref_array( 'post_scan_file', array ( $found, $file, $real_file, &$file_content ) ); |
||
| 353 | } |
||
| 354 |
If you define a variable conditionally, it can happen that it is not defined for all execution paths.
Let’s take a look at an example:
In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.
Available Fixes
Check for existence of the variable explicitly:
Define a default value for the variable:
Add a value for the missing path: