Restrict viewing of uploaded attachments to logged-in users using WordPress and Nginx

Even if you lock down your WordPress site so outside visitors can’t see it, all uploaded attachment such as media, images and PDFs are available to anyone as long as they know the direct link to those files, or can find it by other means such as search engines. Sometimes that’s not acceptable and you want to make sure only logged in users can see uploaded media files.

This approach works by proxying all files in /wp-content/uploads through a script (dl-file.php) which should be placed in the root of your WordPress installation. The script checks whether the user is logged in before serving the file. If the user is not logged in a redirect is performed so that the user can log in before viewing the file.

Because this approach uses the same check as WordPress itself, it should be considered a very safe way of protecting attachments. You could also extend the proxy script to check for different access levels (for example if you have a membership site) and much more!

The Nginx config

# Hotlink protection
location ~ ^/wp-content/uploads/(.*) {
    try_files /dl-file.php =403;
    include fastcgi_params;
    fastcgi_pass php7;
}

This config should be placed in a server block and the example is for servers running EasyEngine, but it should work with minor modification on any php-fpm server.

The proxy script

<?php
/*
 * dl-file.php
 *
 * Protect uploaded files with login.
 *
 * Based on http://wordpress.stackexchange.com/questions/37144/protect-wordpress-uploads-if-user-is-not-logged-in
 * 
 * @author hakre <http://hakre.wordpress.com/> / khromov
 * @license GPL-3.0+
 * @registry SPDX
 */

require_once('wp-load.php');

//Check if we are logged in before attempting to serve the file
is_user_logged_in() || auth_redirect();

//FIXME ... simplify this
list($basedir) = array_values(array_intersect_key(wp_upload_dir(), array('basedir' => 1)))+array(NULL);

//Check if file exists
$getFile = isset($_GET[ 'file' ]) ? $_GET[ 'file' ] : $_SERVER['REQUEST_URI'];

//Normalize file path
$getFile = str_replace('..', '', $getFile);
$getFile = str_replace('wp-content/uploads/', '', $getFile);

//This provides a notice in the log
trigger_error('Loading protected file ' . $getFile);

$file =  trailingslashit($basedir) . $getFile;

//If file is missing, 404 it
if (!$basedir || !is_file($file)) {
    status_header(404);
    die('404 &#8212; 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' ];
else
    $mimetype = 'image/' . substr( $file, strrpos( $file, '.' ) + 1 );

header( 'Content-Type: ' . $mimetype ); // always send this

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

Similar StackOverflow question

Photo by Micah Williams on Unsplash

Leave a Reply

Your email address will not be published. Required fields are marked *

Markdown is allowed in comments.