validate_file() │ WP 1.2.0

Checks a file path (including relative) for forbidden constructs and for compliance with the list of allowed files.

It is used in the core when validating plugin paths, selecting templates, editing files, and unpacking archives.

It is used to prevent Directory traversal attacks.

A successful check does not confirm that the file exists or that the file has access permissions. The function does not check the file contents, extension, and MIME type.

Be careful: if the check passes, the function returns 0, and if it fails, it returns a number. Therefore, you should check like this:

if( validate_file( $file ) === 0 )
	echo 'check passed';
else
	echo 'check did not pass';

Use validate_file_to_edit() when you need to check a path for editing with execution halted via wp_die() on error codes 1 and 3. For code 2, this wrapper returns null.

1 time — 0.000018 sec (very fast) | 50000 times — 0.01 sec (speed of light) | PHP 7.1.2, WP 4.7.5

No Hooks.

Returns

int.

  • 0 - check passed.
  • 1 - a forbidden construct containing ../ was detected.
  • 2 - the second character of the normalized path equals :, for example in C:/file.php.
  • 3 - the path is missing from a non-empty list of allowed files.

Error checks are performed in the order 1, 3, 2. If multiple violations occur, the result corresponds to the first check that triggered.

Usage

validate_file( $file, $allowed_files );
$file(string) (required)

Path to the file.

Before checking, it is normalized via wp_normalize_path(): backslashes are replaced with forward slashes, repeated slashes are reduced except for the two initial ones, and the drive letter is converted to uppercase.

Code 1 occurs if after normalization:

  • The path equals ../.
  • The substring ../ occurs more than once.
  • The only substring ../ is not at the end of the path.

Paths folder/../, folder/.., .. and absolute Unix paths like /etc/passwd are not rejected by themselves. An empty string and non-scalar values, such as null, an array, or an object, immediately result in 0, even if $allowed_files is not empty. Check the type and non-empty value separately.

$allowed_files(array)

Allowed paths. Each element is normalized the same way as $file. Then a strict comparison of the full path with the array elements is performed. File name casing is respected; patterns like *.php are not supported.

An empty array disables this check. The presence of a path in the list does not cancel the other checks.

Default: []

Examples

#1 Demo of file path checks

A path that will pass inspection:

$path = 'uploads/2012/12/my_image.jpg';
echo validate_file( $path ); // print 0 (valid path)

A path that will not pass inspection:

$path = '../../wp-content/uploads/2012/12/my_image.jpg';
echo validate_file( $path ); // print 1 (inaccessible path)

#2 Demonstration

echo validate_file( '' );    // 0
echo validate_file( null );  // 0
echo validate_file( false ); // 0
echo validate_file( 0 );     // 0
echo validate_file( [] );    // 0

echo validate_file( 'image.jpg' );          // 0
echo validate_file( 'uploads/image.jpg' );  // 0
echo validate_file( 'uploads//image.jpg' ); // 0
echo validate_file( '/etc/passwd' );        // 0

echo validate_file( '..' );             // 0
echo validate_file( 'uploads/..' );     // 0
echo validate_file( 'uploads/../' );    // 0
echo validate_file( '../' );            // 1
echo validate_file( '../image.jpg' );   // 1
echo validate_file( 'uploads/../x' );   // 1
echo validate_file( 'uploads/../../' ); // 1
echo validate_file( '..\\image.jpg' );  // 1

echo validate_file( 'C:/image.jpg' );   // 2
echo validate_file( 'C:\\image.jpg' );  // 2
echo validate_file( 'C: image.jpg' );   // 2

$allowed = [ 'uploads/image.jpg' ];

echo validate_file( 'uploads/image.jpg', $allowed );   // 0
echo validate_file( 'uploads\\image.jpg', $allowed );  // 0
echo validate_file( 'uploads//image.jpg', $allowed );  // 0
echo validate_file( 'uploads/IMAGE.jpg', $allowed );   // 3
echo validate_file( 'image.jpg', $allowed );           // 3
echo validate_file( '', $allowed );                    // 0
echo validate_file( null, $allowed );                  // 0

echo validate_file( 'image.jpg', [ '*.jpg' ] );        // 3
echo validate_file( '../image.jpg', $allowed );        // 1
echo validate_file( 'C:/image.jpg', $allowed );        // 3

echo validate_file( '../image.jpg', [ '../image.jpg' ] ); // 1
echo validate_file( 'C:/image.jpg', [ 'C:/image.jpg' ] ); // 2

Changelog

Since 1.2.0 Introduced.

validate_file() code WP 7.1.2

function validate_file( $file, $allowed_files = array() ) {
	if ( ! is_scalar( $file ) || '' === $file ) {
		return 0;
	}

	// Normalize path for Windows servers.
	$file = wp_normalize_path( $file );
	// Normalize path for $allowed_files as well so it's an apples to apples comparison.
	$allowed_files = array_map( 'wp_normalize_path', $allowed_files );

	// `../` on its own is not allowed:
	if ( '../' === $file ) {
		return 1;
	}

	// More than one occurrence of `../` is not allowed:
	if ( preg_match_all( '#\.\./#', $file, $matches, PREG_SET_ORDER ) && ( count( $matches ) > 1 ) ) {
		return 1;
	}

	// `../` which does not occur at the end of the path is not allowed:
	if ( str_contains( $file, '../' ) && '../' !== mb_substr( $file, -3, 3 ) ) {
		return 1;
	}

	// Files not in the allowed file list are not allowed:
	if ( ! empty( $allowed_files ) && ! in_array( $file, $allowed_files, true ) ) {
		return 3;
	}

	// Absolute Windows drive paths are not allowed:
	if ( ':' === substr( $file, 1, 1 ) ) {
		return 2;
	}

	return 0;
}