105

I use WordPress for a private site where users upload files. I use the "Private WordPress" to prevent access in to the site if the user is not logged in.

I would like to do the same to the files uploaded in the uploads folder.

So if a user its not logged in they wont be able to access to : https://xxxxxxx.com/wp-content/uploads/2011/12/xxxxxxx.pdf if they try to access but they are not logged then they should be redirected to login page for example.

I found a plugin called private files but last time updated was in 2009 and it does not seems to work on my WordPress.

Anyone know any method? Hotlinking method will be enough to protect this?

I also found this method :

# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_URI} ^.*uploads/private/.*
RewriteCond %{HTTP_COOKIE} !^.*wordpress_logged_in.*$ [NC]
RewriteRule . /index.php [R,L]
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress

But then any user that replicate the cookie could pass this right? Regards

3
  • 1
    Any reason why you can't use a different upload directory, like one outside of the site root? Commented Dec 22, 2011 at 17:11
  • Not really but i already got tons of files attached to posts in that directory, i dont mind on moving all around if i can find a proper solution
    – chifliiiii
    Commented Dec 22, 2011 at 17:37
  • In case you auto redirect users to login screen, one easy way to protect uploads is to check for referrer. If the referrer is empty (direct access) or different than the domain you host the files, then you block it. Commented Jan 31, 2020 at 17:55

4 Answers 4

112
+50

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.

11
  • 1
    how do you adjust dl-file.php if i want to store files into a subdirectory such as wp-content/uploads/secure ?
    – user11958
    Commented Jan 11, 2012 at 22:54
  • 1
    This is the only really secure solution. Anything else you can find on the web, like checking referer header, checking cookies, disallow directory listing, is a half measure since you can easily spoof HTTP requests headers to get around it.
    – Lukasz
    Commented Sep 20, 2013 at 8:58
  • Guys..this seemed like the perfect solution for me....problem is, I am using PDFJS from Mozilla to access some PDFs from upload folder, and PDFJS uses partial-content headers to get only the pages it is interested in...so this solution is a no goer for me. any suggestions?? Commented Nov 28, 2014 at 17:24
  • @OttoNascarella: Partial Content requests to PHP have been solved as of today, this is independent to this wordpress question. Infact, the question is pretty old already: Resumable downloads when using PHP to send the file?
    – hakre
    Commented Dec 5, 2014 at 10:17
  • 1
    @hakre What about some of those image used on front page of website and any user come to visit site? It gives me 404 error if I am not loggedin. Commented May 1, 2018 at 8:30
21

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 &#8212; 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>
3
  • This didn't work on my side anyone know why? I copy exactly.
    – Ryan S
    Commented Feb 17, 2014 at 10:40
  • Protection working only pdf. other files extension not working like : doc, docx, jpg and etc...
    – Patel
    Commented Aug 23, 2017 at 10:43
  • 1
    Option 2 worked perfectly for all file types, thanks.
    – crdunst
    Commented Mar 5, 2021 at 16:23
2

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/

1
0

What about a plugin-based approach to solving this problem, I've found a WP Plugin created to approach this:

Prevent files / folders access: https://wordpress.org/plugins/prevent-file-access/

Not the answer you're looking for? Browse other questions tagged or ask your own question.