Wordpress - How to Protect Uploads, if User is not Logged In?

Only checking if the cookie exists, is not much of a strict protection.

To get a stronger protection, you can pass or "proxy" all requests to the uploaded folder (exemplary uploads in the following example) through a php script:

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

All requests to uploaded files (which includes images in posts) would go to dl-file.php which then can do verify if the user is logged in or not.

If the user is not logged in, your sites login-form will be shown. After the user logged in, she will get redirected back to the file and can download it now.

Exemplary dl-file.php.

Something similar can be found in \wp-includes\ms-files.php in your wordpress installation, but that one is for multisite and w/o the login check and redirects.

Depending on how much traffic you have, it could be wise to better integrate this with your server, e.g. X-Accel-Redirect or X-Sendfile headers.


Two ways, simple in 2. with the help of an apache rule or in 1. with the help of custom code in a plugin.

1. Plugin

You can write a plugin using the init hook and the get-value $_GET[ 'file' ];. If the user has this get-value, jump in a function to check the rights for access on the files: For example, with a checkbox inside a Meta Box.

add_action( 'init', 'fb_init' );
function fb_init() {
    // this in a function for init-hook
    if ( '' != $_GET[ 'file' ] ) {
        fb_get_file( $_GET[ 'file' ] );
    }
}

the function fb_get_file()

function fb_get_file( $file ) {

    $upload     = wp_upload_dir();
    $the_file   = $file; 
    $file       = $upload[ 'basedir' ] . '/' . $file;
    if ( !is_file( $file ) ) {
        status_header( 404 );
        die( '404 — File not found.' );
    }
    else {
        $image = get_posts( array( 'post_type' => 'attachment', 'meta_query' => array( array( 'key' => '_wp_attached_file', 'value' => $the_file ) ) ) );
        if ( 0 < count( $image ) && 0 < $image[0] -> post_parent ) { // attachment found and parent available
            if ( post_password_required( $image[0] -> post_parent ) ) { // password for the post is not available
                wp_die( get_the_password_form() );// show the password form 
            }
            $status = get_post_meta( $image[0] -> post_parent, '_inpsyde_protect_content', true );

            if ( 1 == $status &&  !is_user_logged_in() ) {
                wp_redirect( wp_login_url( $upload[ 'baseurl' ] . '/' . $the_file ) );
                die();
            }
        }
        else {
            // not a normal attachment check for thumbnail
            $filename   = pathinfo( $the_file );
            $images     = get_posts( array( 'post_type' => 'attachment', 'meta_query' => array( array( 'key' => '_wp_attachment_metadata', 'compare' => 'LIKE', 'value' => $filename[ 'filename' ] . '.' . $filename[ 'extension' ] ) ) ) );
            if ( 0 < count( $images ) ) {
                foreach ( $images as $SINGLEimage ) {
                    $meta = wp_get_attachment_metadata( $SINGLEimage -> ID );
                    if ( 0 < count( $meta[ 'sizes' ] ) ) {
                        $filepath   = pathinfo( $meta[ 'file' ] );
                        if ( $filepath[ 'dirname' ] == $filename[ 'dirname' ] ) {// current path of the thumbnail
                            foreach ( $meta[ 'sizes' ] as $SINGLEsize ) {
                                if ( $filename[ 'filename' ] . '.' . $filename[ 'extension' ] == $SINGLEsize[ 'file' ] ) {
                                    if ( post_password_required( $SINGLEimage -> post_parent ) ) { // password for the post is not available
                                        wp_die( get_the_password_form() );// show the password form 
                                    }
                                    die('dD');
                                    $status = get_post_meta( $SINGLEimage -> post_parent, '_inpsyde_protect_content', true );

                                    if ( 1 == $status &&  !is_user_logged_in() ) {
                                        wp_redirect( wp_login_url( $upload[ 'baseurl' ] . '/' . $the_file ) );
                                        die();
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    $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
        $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'] ) )
        $_SERVER['HTTP_IF_MODIFIED_SINCE'] = false;

    $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 );
        exit;
    }

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

You can also add a custom URL for files via the hook generate_rewrite_rules

add_filter( 'generate_rewrite_rules', 'fb_generate_rewrite_rules' );

function fb_generate_rewrite_rules( $wprewrite ) {
        $upload = wp_upload_dir();
        $path = str_replace( site_url( '/' ), '', $upload[ 'baseurl' ] );
        $wprewrite -> non_wp_rules = array( $path . '/(.*)' => 'index.php?file=$1' );
        return $wprewrite;
}

2. Apache check for the Cookie

Leave a new .htaccess file inside of the /wp-content/uploads/ directory. Or an other defined directory for the uploads.

How it works

Inside of the <IfModule> containers, there are three rules that do the following:

  1. Check if the request is for any file
  2. Check for the absence of a cookie that begins with wordpress_logged_in_
  3. If these conditions are met, the file request will be denied via 403 "Forbidden" response

The trick here is step 2, then check for the absence of a cookie that begins with wordpress_logged_in_. When the user is logged in, WordPress adds a cookie to your browser that looks like:

wordpress_logged_in_1234567890abcdefghijklmnopqrstuvwxyz

Example rule with a check for file type

# require login for media files
<IfModule mod_rewrite.c>
    RewriteCond %{REQUEST_FILENAME} (.*)
    RewriteCond %{HTTP_COOKIE} !wordpress_logged_in_([a-zA-Z0-9_]*) [NC]
    RewriteRule .* - [F,L]
</IfModule>

If you would like a plugin-based approach to solving this problem, here is a reasonably good solution that I have (finally) found:

  1. Install the plugin 'Download Monitor', available at:
    https://wordpress.org/plugins/download-monitor/
  2. In the WordPress Dashboard, go to the new 'Downloads' menu item and add a new 'Download', as described on the plugin documentation website here: https://www.download-monitor.com/kb/adding-downloads/. Take note of the 'Download' shortcode provided for you (eg. save to Notepad). Note that the file gets saved in /wp-content/uploads/dlm_uploads/
  3. In the 'Download options' metabox, specify 'Members only' (as documented here https://www.download-monitor.com/kb/download-options/), and click 'Publish'.
  4. On the page that you want the Members only download to appear, add in the shortcode you took note of in step #2, and 'Publish/Update' the page, as documented here: https://www.download-monitor.com/kb/shortcode-download/. You can change the download link template as described here https://www.download-monitor.com/kb/content-templates/, or create your own (eg. to remove the Download 'count')
  5. Browse to your page, you should see a download link (but which does not reveal the URL to the download file). If you browse to the same page in a new browser window (or Incognito window), you should find that the download no longer works.

This means that anyone not logged in cannot either download the file or see the real URL to the file. If in the event that someone unauthorised figures out the URL to the file, the plugin also stops users browsing to the real file URL by blocking access to the /wp-content/uploads/dlm_uploads/ folder.

Bonus: if you a doing this for a site where you need users to be able to login as 'Members' only (but have no WordPress permissions like page editing or being an Admin), install the 'Members' plugin https://wordpress.org/plugins/members/, create a new user role called 'Member', and give it the single capability of 'read', create a new User in WordPress, and make sure to give them a role of 'Member'.

If you want protect the content of pages, the 'Members' plugin provides some options, or there are other plugins out there. If you want to theme the login page for Members to look better than the WordPress default login form, use something like 'Theme My Login': https://wordpress.org/plugins/theme-my-login/