Created
October 31, 2018 03:58
-
-
Save firxworx/eb004e78abe3fc1ccdd2770e7edfcc65 to your computer and use it in GitHub Desktop.
Restrict access to files in the WordPress uploads folder so they can only be accessed by logged-in users
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# The following Apache .htaccess snippet should be included at the top of the .htaccess file in WordPress' root directory. | |
# | |
# Use this option only if you wish to manually modify the .htaccess file. To fully contain file restriction functionality | |
# inside a plugin you can use the mod_rewrite_rules filter as demonstrated in a commented-out block in plugin-snippet.php | |
# | |
# Note <IfModule mod_rewrite.c></IfModule> test is intentionally omitted to trigger an error if mod_rewrite is not available. | |
# BEGIN RestrictFileExample | |
# it is ok if WordPress also calls these two directives later on in the file | |
RewriteEngine On | |
RewriteBase / | |
# force ssl (optional per your environment -- may need to be excluded in your dev environment) | |
RewriteCond %{HTTPS} off | |
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] | |
# rewrite all requests for regular files with nontrivial size to pass the filename as the fx_target_file arg | |
RewriteCond %{REQUEST_FILENAME} -s | |
RewriteRule ^wp-content/uploads/(.*)$ /index.php?fx_target_file=$1 [QSA,L] | |
# END RestrictFileExample |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* The following functions are provided for example purposes and should ideally be encapsulated | |
* within a class and/or used with a namespace. | |
*/ | |
/** | |
* Add query var `fx_target_file` for restricted file access | |
* | |
* filter: query_vars | |
*/ | |
function addFileQueryVarsFilter( $query_vars ) { | |
$query_vars[] = 'fx_target_file'; | |
return $query_vars; | |
} | |
add_filter( 'query_vars', 'addFileQueryVarsFilter' ); | |
/** | |
* Disrupt the WordPress' load process if the `fx_target_file` query var is set: include restrict.php and exit. | |
* Note that `parse_request` runs before `parse_query` so get_query_var() will not be available within restrict.php | |
* | |
* action: parse_request | |
*/ | |
function restrictedFileRequestAction( $wp ) { | |
if ( array_key_exists( 'fx_target_file', $wp->query_vars ) ) { | |
include( dirname( plugin_dir_path( __FILE__ ) ) . '/restrict.php' ); | |
exit(); | |
} | |
} | |
add_action( 'parse_request', 'restrictedFileRequestAction' ); | |
/** | |
* The following demonstrates how to modify WordPress' .htaccess file and add the required rewrite rules from within a plugin. | |
* The mod_rewrite_rules filter is invoked after WordPress has generated its rules and before they are written to the .htaccess file. | |
* Note that you must hard flush permalinks in order to write/commit these rules to the .htaccess file. | |
* Flushing permalinks is an expensive operation and should not be done for every request. | |
* For maximum portability use wp_upload_dir() rather than hard-code the uploads folder per the example below. | |
* | |
* function prependFileHtaccessRules( $rules ) { | |
* $inject = <<<"EOD" | |
* \n## Plugin Restrict File Access ## | |
* RewriteEngine On | |
* RewriteBase / | |
* RewriteCond %{REQUEST_FILENAME} -s | |
* RewriteRule ^wp-content/uploads/(.*)$ /index.php?fx_target_file=$1 [QSA,L] | |
* ## Plugin Restrict File Access ##\n | |
* EOD; | |
* return $inject . $rules; | |
* } | |
* add_filter( 'mod_rewrite_rules', 'prependFileHtaccessRules' ); | |
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* restrict.php | |
* | |
* Protect files in the WordPress uploads folder so they can only be accessed/downloaded by logged-in users. | |
* | |
* This gist can be easily customized to more tailored access restrictions (e.g. user role/capability). | |
* ETAG is intentionally not supported for security. | |
* | |
* This gist example improves upon other methods seen online because it can be fully implemented within a self-contained | |
* plugin including this file (restrict.php) and writing the required .htaccess rules. | |
* | |
* It also proposes a more secure approach than flawed methods proposed in other snippets that I've seen, such as | |
* when 'secure' code only checks for the existence of a cookie (among others). | |
* | |
* Creating this gist was inspired by a StackExchange topic: | |
* @link http://wordpress.stackexchange.com/questions/37144/protect-wordpress-uploads-if-user-is-not-logged-in | |
* It was also inspired by an implementation by @hakre and the numerous commenters on the associated gist: | |
* @link https://gist.github.com/hakre/1552239 | |
* A chunk of @hakre's implementation is sourced from the following WP core file: /wp-includes/ms-files.php | |
* | |
* October 2018 | |
* | |
* @author Kevin Firko | |
* @license GPL-3.0+ | |
* | |
*/ | |
// turn off output buffering (if on) to prevent exhausting memory when handling large files | |
if ( ob_get_level() ) { // see 'Notes' for the readfile() function in php docs | |
ob_end_clean(); | |
} | |
// verify the request is from a logged-in user | |
// you can customize access restrictions/permissions here, e.g. test current_user_can( 'example_capability' ); | |
if (! is_user_logged_in() ) { | |
wp_die( 'Unauthorized', 401 ); | |
} | |
// confirm WordPress has a valid upload directory | |
$uploadDir = wp_upload_dir(); | |
if ( empty( $uploadDir ) || !empty( $uploadDir['error'] ) ) { | |
status_header( 500 ); | |
exit; | |
} | |
// determine target filename (note use of $_GET because get_query_var() is only available later in WP's loading sequence) | |
$basedir = $uploadDir['basedir']; | |
$file = $_GET['fx_target_file']; | |
$file = rtrim( $basedir, '/' ) . '/' . $file; | |
$fileRealPath = realpath( $file ); | |
// confirm the inferred requested file path and fileRealPath match (re potential directory traversal attacks) | |
if ( $fileRealPath !== $file ) { | |
status_header( 403 ); | |
exit; | |
} | |
// confirm valid real path and file | |
if ( ( FALSE === $fileRealPath ) || !is_file( $file ) ) { | |
status_header( 404 ); | |
exit; | |
} | |
// determine MIME type | |
$mime = wp_check_filetype( $file ); | |
if ( FALSE === $mime[ 'type' ] && function_exists( 'mime_content_type' ) ) { | |
$mime[ 'type' ] = mime_content_type( $file ); | |
} | |
if ( $mime[ 'type' ] ) { | |
$mimeType = $mime[ 'type' ]; | |
} else { | |
// if undetermined assume an image and set MIME type based on file extension (similar to WP core) | |
$mimeType = 'image/' . substr( $file, strrpos( $file, '.' ) + 1 ); | |
// handle jpg special case | |
if ( $mimeType = 'image/jpg' ) { | |
$mimeType = 'image/jpeg'; | |
} | |
} | |
// set headers | |
header( 'Content-Type: ' . $mimeType ); | |
if ( FALSE === strpos( $_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS' ) ) { | |
header( 'Content-Length: ' . filesize( $file ) ); | |
} | |
// send the file | |
readfile( $file ); | |
flush(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment