Improve handling of relative paths in downloadable files
to prevent access to files outside of WordPress uploads folder.
This commit is contained in:
parent
efe7e4f8ba
commit
b5c5a4da15
|
@ -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'];
|
||||
}
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
|
||||
|
|
|
@ -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() );
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue