Files

99 lines
3.1 KiB
PHP
Raw Permalink Normal View History

<?php
/**
* Verify that every shipped PHP file really guards against direct access.
*
* 🔴 Tokenised, not grepped, and that is the whole point of the file.
*
* The grep version asked whether "defined( 'ABSPATH' )" appeared ANYWHERE in
* the file. A comment saying the guard is handled elsewhere therefore passed,
* and so did a file with the words and no guard at all. Measured: a planted
* file whose only mention was in a comment produced
* "15 shipped PHP files examined, every one guarded" — green over a file that
* runs for anyone who fetches it directly, outside WordPress and outside every
* capability check the code assumes has already happened.
*
* A check that must RECOGNISE its own code has one blind spot per recognition
* method; one that reads what the source actually says has none of that
* particular kind. PHP's own tokeniser cannot be fooled by a comment, because
* it is the thing that decides what a comment is.
*/
$min = 0;
$files = array();
foreach ( array_slice( $argv, 1 ) as $arg ) {
if ( 0 === strpos( $arg, '--min=' ) ) {
$min = (int) substr( $arg, 6 );
continue;
}
$files[] = $arg;
}
$guards = array( 'ABSPATH', 'WP_UNINSTALL_PLUGIN' );
$problems = 0;
$checked = 0;
foreach ( $files as $path ) {
if ( ! is_readable( $path ) ) {
printf( "::error file=%s::Listed as shipped but not readable.\n", $path );
++$problems;
continue;
}
++$checked;
$tokens = token_get_all( file_get_contents( $path ) );
$count = count( $tokens );
$found = false;
for ( $i = 0; $i < $count; $i++ ) {
$token = $tokens[ $i ];
// Comments and doc blocks are tokens of their own, so they are simply
// never examined. That is the property the grep version lacked.
if ( ! is_array( $token ) || T_STRING !== $token[0] || 'defined' !== strtolower( $token[1] ) ) {
continue;
}
// defined ( 'ABSPATH' ) — whitespace anywhere, quotes either kind.
$j = $i + 1;
while ( $j < $count && is_array( $tokens[ $j ] ) && T_WHITESPACE === $tokens[ $j ][0] ) {
++$j;
}
if ( $j >= $count || '(' !== $tokens[ $j ] ) {
continue;
}
++$j;
while ( $j < $count && is_array( $tokens[ $j ] ) && T_WHITESPACE === $tokens[ $j ][0] ) {
++$j;
}
if ( $j < $count && is_array( $tokens[ $j ] ) && T_CONSTANT_ENCAPSED_STRING === $tokens[ $j ][0] ) {
if ( in_array( trim( $tokens[ $j ][1], "'\"" ), $guards, true ) ) {
$found = true;
break;
}
}
}
if ( ! $found ) {
printf(
"::error file=%s,line=1::No guard against direct access. Add: if ( ! defined( 'ABSPATH' ) ) { exit; }\n",
$path
);
++$problems;
}
}
// The floor, for the same reason every other check in this kit has one: "every
// shipped file is guarded" is also what zero shipped files prints.
if ( $checked < $min ) {
fwrite( STDERR, sprintf( "Only %d shipped PHP file(s) were examined, expected at least %d. The file list is wrong, so this check proved nothing.\n", $checked, $min ) );
exit( 1 );
}
if ( $problems > 0 ) {
printf( "\n%d of %d shipped PHP files have no guard against direct access.\n", $problems, $checked );
exit( 1 );
}
printf( " %d shipped PHP files parsed, every one guards against direct access\n", $checked );
exit( 0 );