Improve handling of relative paths in downloadable files

to prevent access to files outside of WordPress uploads folder.
This commit is contained in:
Nestor Soriano 2021-11-05 13:43:22 +01:00
parent efe7e4f8ba
commit b5c5a4da15
No known key found for this signature in database
GPG Key ID: 08110F3518C12CAD
3 changed files with 66 additions and 15 deletions

View File

@ -262,16 +262,17 @@ class WC_Download_Handler {
* via filters we can still do the string replacement on a HTTP file.
*/
$replacements = array(
$wp_uploads_url => $wp_uploads_dir,
network_site_url( '/', 'https' ) => ABSPATH,
$wp_uploads_url => $wp_uploads_dir,
network_site_url( '/', 'https' ) => ABSPATH,
str_replace( 'https:', 'http:', network_site_url( '/', 'http' ) ) => ABSPATH,
site_url( '/', 'https' ) => ABSPATH,
str_replace( 'https:', 'http:', site_url( '/', 'http' ) ) => ABSPATH,
site_url( '/', 'https' ) => ABSPATH,
str_replace( 'https:', 'http:', site_url( '/', 'http' ) ) => ABSPATH,
);
$count = 0;
$file_path = str_replace( array_keys( $replacements ), array_values( $replacements ), $file_path );
$parsed_file_path = wp_parse_url( $file_path );
$remote_file = true;
$remote_file = null === $count || 0 === $count; // Remote file only if there were no replacements.
// Paths that begin with '//' are always remote URLs.
if ( '//' === substr( $file_path, 0, 2 ) ) {
@ -291,7 +292,7 @@ class WC_Download_Handler {
$file_path = realpath( WP_CONTENT_DIR . substr( $file_path, 11 ) );
// Check if we have an absolute path.
} elseif ( ( ! isset( $parsed_file_path['scheme'] ) || ! in_array( $parsed_file_path['scheme'], array( 'http', 'https', 'ftp' ), true ) ) && isset( $parsed_file_path['path'] ) && file_exists( $parsed_file_path['path'] ) ) {
} elseif ( ( ! isset( $parsed_file_path['scheme'] ) || ! in_array( $parsed_file_path['scheme'], array( 'http', 'https', 'ftp' ), true ) ) && isset( $parsed_file_path['path'] ) ) {
$remote_file = false;
$file_path = $parsed_file_path['path'];
}

View File

@ -7,6 +7,8 @@
* @since 3.0.0
*/
use Automattic\Jetpack\Constants;
defined( 'ABSPATH' ) || exit;
/**
@ -98,18 +100,28 @@ class WC_Product_Download implements ArrayAccess {
$file_path = $this->get_file();
// File types for URL-based files located on the server should get validated.
$is_file_on_server = false;
if ( false !== stripos( $file_path, network_site_url( '/', 'https' ) ) ||
false !== stripos( $file_path, network_site_url( '/', 'http' ) ) ||
false !== stripos( $file_path, site_url( '/', 'https' ) ) ||
false !== stripos( $file_path, site_url( '/', 'http' ) )
) {
$is_file_on_server = true;
}
$parsed_file_path = WC_Download_Handler::parse_file_path( $file_path );
$is_file_on_server = ! $parsed_file_path['remote_file'];
$file_path_type = $this->get_type_of_file_path( $file_path );
if ( ! $is_file_on_server && 'relative' !== $this->get_type_of_file_path() ) {
// Shortcodes are allowed, validations should be done by the shortcode provider in this case.
if ( 'shortcode' === $file_path_type ) {
return true;
}
// Remote paths are allowed.
if ( ! $is_file_on_server && 'relative' !== $file_path_type ) {
return true;
}
// On windows system, local files ending with `.` are not allowed.
// @link https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#naming-conventions.
if ( $is_file_on_server && ! $this->get_file_extension() && 'WIN' === strtoupper( substr( Constants::get_constant( 'PHP_OS' ), 0, 3 ) ) ) {
if ( '.' === substr( $file_path, -1 ) ) {
return false;
}
}
return ! $this->get_file_extension() || in_array( $this->get_file_type(), $this->get_allowed_mime_types(), true );
}

View File

@ -0,0 +1,38 @@
<?php
/**
* Class WC_Product_Download_Test
*/
class WC_Product_Download_Test extends WC_Unit_Test_Case {
/**
* Test for file without extension.
*/
public function test_is_allowed_filetype_with_no_extension() {
$upload_dir = trailingslashit( wp_upload_dir()['basedir'] );
$file_path_with_no_extension = $upload_dir . 'upload_file';
if ( ! file_exists( $file_path_with_no_extension ) ) {
// Copy an existing file without extension.
$this->assertTrue( touch( $file_path_with_no_extension ), 'Unable to create file without extension.' );
}
$download = new WC_Product_Download();
$download->set_file( $file_path_with_no_extension );
$this->assertEquals( true, $download->is_allowed_filetype() );
}
/**
* Simulates test condition for windows when filename ends with a period.
*/
public function test_is_allowed_filetype_on_windows_with_period_at_end() {
$upload_dir = trailingslashit( wp_upload_dir()['basedir'] );
$file_path_with_period_at_end = $upload_dir . 'upload_file.';
if ( ! file_exists( $file_path_with_period_at_end ) ) {
// Copy an existing file without extension.
$this->assertTrue( touch( $file_path_with_period_at_end ), 'Unable to create file with period at the end.' );
}
\Automattic\Jetpack\Constants::set_constant( 'PHP_OS', 'winnt' );
$download = new WC_Product_Download();
$download->set_file( $file_path_with_period_at_end );
$this->assertEquals( false, $download->is_allowed_filetype() );
}
}