Protecting WordPress Media Uploads Unless User is Logged In


Sometimes developing even the most simplistic solutions can be difficult. We have worked with several clients that have requested to keep their uploads folder private. While there are some plugin solutions that may assist in this – I figured I would post a simplistic way to do this manually to keep your code clean.

How does it work?
We will be modifying the .htaccess file in the root of your WordPress directory and telling it to redirect uploaded files if a user is not logged in. We will also add a redirect parameter to tell WordPress how to handle users so they will be correctly redirected to the file after logging in.
**Note: If you are using a custom plugin for front-end login screens (such as Profile Builder) you will need to modify the code a bit to pass a redirect parameter to that login screen, but this should give you a good start. **

WordPress and .HTACCESS
WordPress will generate an .htaccess file when you change your permalink structure. Because of this behavior, we need to make sure that we understand how to input custom htaccess rules in the file so that WordPress will not overwrite them when changing this structure. Let’s get into the code.
**Note: This tutorial assumes that you keep all your uploads in the same folder by unchecking “Organize my uploads into month- and year-based folders”. **

The code
Navigate to your .htaccess file via FTP in your WordPress root. If you do not see one – login to WordPress and update your permalink structure (Settings -> Permalinks -> Choose Post name). Now that you have an .htaccess file, edit it. We will be adding this code to our .htaccess file:

RewriteCond %{REQUEST_FILENAME} -s
RewriteRule ^wp-content/uploads/(.*)$ dl-file.php?file=$1 [QSA,L]

Make sure to add the code above the generated code that WordPress uses below (anything outside this WordPress will not touch).

# BEGIN WordPress

RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]

# END WordPress

Okay, now what?
Now we have control – we can do anything we want when an uploaded file is accessed. Each time an uploaded file is accessed we are telling it to run code we will create in a file named “dl-file.php”. Create a file named “dl-file.php” (without the quotes) in the root of your WordPress directory. Now, add this code inside the file to control our uploads:


If (!is_user_logged_in()){
$upload_dir = wp_upload_dir();
echo $upload_dir['baseurl'] . '/' . $_GET[ 'file' ];
wp_redirect( wp_login_url( $upload_dir['baseurl'] . '/' . $_GET[ 'file' ]));
list($basedir) = array_values(array_intersect_key(wp_upload_dir(), array('basedir' => 1)))+array(NULL);

$file = rtrim($basedir,'/').'/'.str_replace('..', '', isset($_GET[ 'file' ])?$_GET[ 'file' ]:'');
if (!$basedir || !is_file($file)) {
die('404 — File not found.');

$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’ ];
$mimetype = ‘image/’ . substr( $file, strrpos( $file, ‘.’ ) + 1 );

header( ‘Content-Type: ‘ . $mimetype ); // always send this
if ( false === strpos( $_SERVER[‘SERVER_SOFTWARE’], ‘Microsoft-IIS’ ) )
header( ‘Content-Length: ‘ . filesize( $file ) );

$last_modified = gmdate( ‘D, d M Y H:i:s’, filemtime( $file ) );
$etag = ‘”‘ . md5( $last_modified ) . ‘”‘;
header( “Last-Modified: $last_modified GMT” );
header( ‘ETag: ‘ . $etag );
header( ‘Expires: ‘ . gmdate( ‘D, d M Y H:i:s’, time() + 100000000 ) . ‘ GMT’ );

// Support for Conditional GET
$client_etag = isset( $_SERVER[‘HTTP_IF_NONE_MATCH’] ) ? stripslashes( $_SERVER[‘HTTP_IF_NONE_MATCH’] ) : false;

if( ! isset( $_SERVER[‘HTTP_IF_MODIFIED_SINCE’] ) )

$client_last_modified = trim( $_SERVER[‘HTTP_IF_MODIFIED_SINCE’] );
// If string is empty, return 0. If not, attempt to parse into a timestamp
$client_modified_timestamp = $client_last_modified ? strtotime( $client_last_modified ) : 0;

// Make a timestamp for our most recent modification…
$modified_timestamp = strtotime($last_modified);

if ( ( $client_last_modified && $client_etag )
? ( ( $client_modified_timestamp >= $modified_timestamp) && ( $client_etag == $etag ) )
: ( ( $client_modified_timestamp >= $modified_timestamp) || ( $client_etag == $etag ) )
) {
status_header( 304 );

// If we made it this far, just serve the file
readfile( $file );

What exactly did we just do?
The first line of code


is just telling the PHP file to load the necessary files to call WordPress functions.

The next little snippet is the key:

If (!is_user_logged_in()){
$upload_dir = wp_upload_dir();
echo $upload_dir['baseurl'] . '/' . $_GET[ 'file' ];
wp_redirect( wp_login_url( $upload_dir['baseurl'] . '/' . $_GET[ 'file' ]));

We are validating that the user is not logged in (if you have certain users you need to restrict you can modify this). Since the user is not logged in, we will redirect the user to the login page. Once the user logs in, it will automatically redirect him/her to the file. Although some modifications were made to the code, original credit goes to

Hope this helps!

Travis Hoglund
Zer0 to 5ive Senior Developer