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

Post a Comment

You must be logged in to post a comment.

(6) Responses to “Protecting WordPress Media Uploads Unless User is Logged In”

  1. hand_coding says:

    I’m desperately trying to adapt this script to another directory than “uploads” (like a sub-directory). I can’t figure how to deal with the “$upload_dir = wp_upload_dir();” part to change the path.
    Please help…

  2. The problem with adapting the script is understanding functions that return objects, absolute paths, relative paths, and absolute URLs. For instance:

    wp_upload_dir(); is returning an object with these values:

    $upload_dir = wp_upload_dir(); // Array of key => value pairs
    //$upload_dir is now an array that contains something like the following (if successful)
    Array (
    [path] => C:pathtowordpresswp-contentuploads20105
    [url] =>
    [subdir] => /2010/05
    [basedir] => C:pathtowordpresswp-contentuploads
    [baseurl] =>
    [error] =>

    You can use any of the above by doing $upload_dir[‘baseurl’] for instance which would return [C:pathtowordpresswp-contentuploads] in this case. So where you see my script using that object you can manually use your own values if you wish.

    Let me know if this helps!


  3. hand_coding says:

    Hi Travis,
    Thank you for taking the time to respond.
    Yes, wp_upload_dir() returns an array, as print_r told me. That’s what makes me struggle with a single value variable.
    I can’t work on it for now, but I’ll keep you informed.
    Thanks again for your explanations.

  4. Danyo says:

    Great Tutorial!

    Is there a way to restrict the files to certain users?

    I would like to traget the post_author, and only they can see and view files they have uploaded. So they will need to be logged in and also the author of the post attachment. Is this something that can be done with the above?


  5. Ben says:

    i searched for hours to restrict access to upload folder and you give here a clean and quick solution : thanks !

  6. Chris says:

    Hi Travis. This is a great post. I would like to be able to leave the /uploads directory alone and protect a different directory with the script in this post. Could you help clarify something for me? I would like to have a directory such as wp-content/docs where I have some files uploaded to and use this to check for user being logged in. Can you email me or respond with the modifications that would be necessary to my .htaccess file and dl-file.php

    Thank you.