How to Protect Your Uploaded Files From Unauthorized Users

Contents and files are the main assets of your website/business. In some case/business you may want to show your content only for websites members who are logged in. Assume you are showing a video/audio/image in your post content and you want to allow this file for only logged in users or some specific users or groups. Or you want to sell some downloadable files and your authorized users can download the file. Ok, your php/wordpress script generates your protected page only for authorized people. And your content contains the embedded media files or download links. But this file is saved somewhere in your host and it must has a static direct URL. Now your authorized user visit your protected page and may copy the file url or download link and shares with others. Then anyone can download/see your files. Am I right? I built a technique to protect files from unauthorized visitor though they know the direct file URL. No problem if someone know your file url. In this article I will show you how do I protect my files.
Here I am using HTACCESS and PHP Session technique and all codes are PHP and WordPress based.
Lock Your Uploaded Files

Entire Process at a Glance

Assume your protected files are stored in wp-content/uploades/protected/ directory. You want to give access to these files to only authorized users. The first thing you have to do, Create a .htaccess file in this directory. Redirect all request in this directory to another php script. I named this php file ‘locker.php’. You can store this locker.php anywhere in your script. If you want to make an wordpress plugin , you can store it in your plugin directory.
In htaccess file write your protected directory path ( relative/absolute) for RewriteBase and write the full path(absolute/relative) of locker.php for RewriteRule
Now all request to /protected directory will go through locker.php
Then you have to check the visitor who called the request is authorized or not. As an wordpress developer you want to load full wp in your locker.php to check the user logged in or not. But.. I don’t like to load whole wordpress for all case. And you may have to use this process in not wp websites.
So how can you check the user is logged-in/authorized or not without loading wordpress core system?
My approach is different here.
Before calling a protected file visitor must have to visit your frontend webpage which page contains your protected file link.
Consider you have a page YOURSITE/download. In this download page you will show the file link if the user is permitted to the file. Now apply my technique. Before generating page contents do some php session works. If the visitor is permitted to your files create a session and store the file url in the $_SESSION[] variable. You are done for this step.
Then when your visitor will click the download link or browser will call your protected images/videos , the request will hit the htaccess and htaccess file will redirect the request to your predefined locker.php. Now your locker.php will check the requested url and session var. If it found the requested file url is exists in the session then it will read the file and send to browser. Otherwise this script will return http message ‘not found’ or ‘access dented’ or what you prefer.

I divided this whole project in three parts.

  • Part 1: HTACCESS
  • Part 2: Session Creator Script
  • Part 3: File Serve Script

HTACCESS

In my case htaccess file is bellow.


<IfModule mod_rewrite.c>
    RewriteEngine on
    RewriteBase /wp-content/uploads/protected
    RewriteRule ^(.*)$ ../../plugins/my-file-protection-plugin/locker.php?p=$1 [L,QSA]
</IfModule>

Session Creator Script

In my case, I store my files in separated unique directory in my protected directory. And I identified my files by their directory name. As example:
http://example.com/wp-content/uploades/protected/project1/guide.pdf
http://example.com/wp-content/uploades/protected/project2/guide.pdf
http://example.com/wp-content/uploades/protected/project3/guide.pdf
Here I check only if the user is logged in or not. If the user is logged in I store the file directory name into session variable and then my locker.php will check this session value. You can check not only loggin but also many other permission as you need. For this section I am using following codes.

<?php
	#checking the authentication
	if ( is_user_logged_in() )
	{
	$allowed_to_content=true;
	}

	#saving session
	if(!session_id()) session_start();
	$my_protected_file_url="http://example.com/wp-content/uploades/protected/project1/guide.php";
	#avove var will by dynamic
	if($my_protected_file_url)
	{
		$oneTarek_content_dir_name="protected";
		$oneTarek_content_access_session=$_SESSION['oneTarek_content_access'];
		if(!is_array($oneTarek_content_access_session)){$oneTarek_content_access_session=array();}

		$temp=explode($oneTarek_content_dir_name."/",$my_protected_file_url);
		$temp2=$temp[1];
		$temp3=explode("/",$temp2);
		$content_directory_name=$temp3[0];
		if($allowed_to_content)
		{
		$oneTarek_content_access_session[$content_directory_name]="true";
		}
		else
		{
		$oneTarek_content_access_session[$content_directory_name]="false";
		}

		$_SESSION['oneTarek_content_access']=$oneTarek_content_access_session;
	}# end if($my_protected_file_url)

?>

File Serve Script

I am using this project with my wordpress plugin. So I stored my locker.php in my plugin directory. As I store my files in separate unique directory so in locker.php I search directory name in session variable. You can store different type of session value as you need to identify your files location. My locker.php is bellow.
locker.php

wp-content/plugins/my-file-protection-plugin/locker.php

<?php
#written by oneTarek : http://onetarek.com  please keep this line if you want to use this file.
#this locker.php is stored in wp-content/plugins/my-file-protection-plugin/
#so all path are being used by reletivly with this plugin path.

define('WP_CONTENT_DIR_NAME','wp-content'); #CHANGE THIS IF YOUR content directory is defferent.
define('WP_UPLOADS_DIR_NAME','uploads');
define('WP_ONETAREK_UPLOADS_DIR_NAME','protected');

if(!session_id()) session_start();

#checking the content access permission;
$oneTarek_content_access_session=$_SESSION['oneTarek_content_access'];

if(!is_array($oneTarek_content_access_session)){$oneTarek_content_access_session=array();}

$oneTarek_content_dir_name=WP_ONETAREK_UPLOADS_DIR_NAME; #declared in settings.php
	$temp=explode($oneTarek_content_dir_name."/",urldecode($_SERVER['REQUEST_URI']));
	$temp2=$temp[1];
	$temp3=explode("/",$temp2);
	$content_directory_name=$temp3[0];
	if(!array_key_exists($content_directory_name,$oneTarek_content_access_session))
	{
	status_header(203);
	die('203 &#8212; Non-Authoritative Information.');
	}
	if($oneTarek_content_access_session[$content_directory_name]!='true')
	{
	status_header(203);
	die('203 &#8212; Non-Authoritative Information.');
	}

$fl_explode=explode($oneTarek_content_dir_name."/",urldecode($_SERVER['REQUEST_URI']));
$filename=$fl_explode[count($fl_explode)-1];

$file=dirname(__FILE__)."/../../".WP_UPLOADS_DIR_NAME."/".$oneTarek_content_dir_name."/".$filename;

if (!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 ( 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 );

#******************SOME MODIFIED WORDPRESS FUNCTIONS*********

#function source: wp-includes/functions.php
function wp_check_filetype( $filename, $mimes = null ) {
	if ( empty($mimes) )
		$mimes = get_allowed_mime_types();
	$type = false;
	$ext = false;

	foreach ( $mimes as $ext_preg => $mime_match ) {
		$ext_preg = '!\.(' . $ext_preg . ')$!i';
		if ( preg_match( $ext_preg, $filename, $ext_matches ) ) {
			$type = $mime_match;
			$ext = $ext_matches[1];
			break;
		}
	}

	return compact( 'ext', 'type' );
}

function get_allowed_mime_types() {
	static $mimes = false;

	if ( !$mimes ) {
		// Accepted MIME types are set here as PCRE unless provided.
		$mimes = array(
		'jpg|jpeg|jpe' => 'image/jpeg',
		'gif' => 'image/gif',
		'png' => 'image/png',
		'bmp' => 'image/bmp',
		'tif|tiff' => 'image/tiff',
		'ico' => 'image/x-icon',
		'asf|asx|wax|wmv|wmx' => 'video/asf',
		'avi' => 'video/avi',
		'divx' => 'video/divx',
		'flv' => 'video/x-flv',
		'mov|qt' => 'video/quicktime',
		'mpeg|mpg|mpe' => 'video/mpeg',
		'txt|asc|c|cc|h' => 'text/plain',
		'csv' => 'text/csv',
		'tsv' => 'text/tab-separated-values',
		'ics' => 'text/calendar',
		'rtx' => 'text/richtext',
		'css' => 'text/css',
		'htm|html' => 'text/html',
		'mp3|m4a|m4b' => 'audio/mpeg',
		'mp4|m4v' => 'video/mp4',
		'ra|ram' => 'audio/x-realaudio',
		'wav' => 'audio/wav',
		'ogg|oga' => 'audio/ogg',
		'ogv' => 'video/ogg',
		'mid|midi' => 'audio/midi',
		'wma' => 'audio/wma',
		'mka' => 'audio/x-matroska',
		'mkv' => 'video/x-matroska',
		'rtf' => 'application/rtf',
		'js' => 'application/javascript',
		'pdf' => 'application/pdf',
		'doc|docx' => 'application/msword',
		'pot|pps|ppt|pptx|ppam|pptm|sldm|ppsm|potm' => 'application/vnd.ms-powerpoint',
		'wri' => 'application/vnd.ms-write',
		'xla|xls|xlsx|xlt|xlw|xlam|xlsb|xlsm|xltm' => 'application/vnd.ms-excel',
		'mdb' => 'application/vnd.ms-access',
		'mpp' => 'application/vnd.ms-project',
		'docm|dotm' => 'application/vnd.ms-word',
		'pptx|sldx|ppsx|potx' => 'application/vnd.openxmlformats-officedocument.presentationml',
		'xlsx|xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml',
		'docx|dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml',
		'onetoc|onetoc2|onetmp|onepkg' => 'application/onenote',
		'swf' => 'application/x-shockwave-flash',
		'class' => 'application/java',
		'tar' => 'application/x-tar',
		'zip' => 'application/zip',
		'gz|gzip' => 'application/x-gzip',
		'rar' => 'application/rar',
		'7z' => 'application/x-7z-compressed',
		'exe' => 'application/x-msdownload',
		// openoffice formats
		'odt' => 'application/vnd.oasis.opendocument.text',
		'odp' => 'application/vnd.oasis.opendocument.presentation',
		'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
		'odg' => 'application/vnd.oasis.opendocument.graphics',
		'odc' => 'application/vnd.oasis.opendocument.chart',
		'odb' => 'application/vnd.oasis.opendocument.database',
		'odf' => 'application/vnd.oasis.opendocument.formula',
		// wordperfect formats
		'wp|wpd' => 'application/wordperfect',
		);
	}

	return $mimes;
}

function get_status_header_desc( $code ) {
	$code = abs( intval( $code ) );
		$wp_header_to_desc = array(
			100 => 'Continue',
			101 => 'Switching Protocols',
			102 => 'Processing',

			200 => 'OK',
			201 => 'Created',
			202 => 'Accepted',
			203 => 'Non-Authoritative Information',
			204 => 'No Content',
			205 => 'Reset Content',
			206 => 'Partial Content',
			207 => 'Multi-Status',
			226 => 'IM Used',

			300 => 'Multiple Choices',
			301 => 'Moved Permanently',
			302 => 'Found',
			303 => 'See Other',
			304 => 'Not Modified',
			305 => 'Use Proxy',
			306 => 'Reserved',
			307 => 'Temporary Redirect',

			400 => 'Bad Request',
			401 => 'Unauthorized',
			402 => 'Payment Required',
			403 => 'Forbidden',
			404 => 'Not Found',
			405 => 'Method Not Allowed',
			406 => 'Not Acceptable',
			407 => 'Proxy Authentication Required',
			408 => 'Request Timeout',
			409 => 'Conflict',
			410 => 'Gone',
			411 => 'Length Required',
			412 => 'Precondition Failed',
			413 => 'Request Entity Too Large',
			414 => 'Request-URI Too Long',
			415 => 'Unsupported Media Type',
			416 => 'Requested Range Not Satisfiable',
			417 => 'Expectation Failed',
			422 => 'Unprocessable Entity',
			423 => 'Locked',
			424 => 'Failed Dependency',
			426 => 'Upgrade Required',

			500 => 'Internal Server Error',
			501 => 'Not Implemented',
			502 => 'Bad Gateway',
			503 => 'Service Unavailable',
			504 => 'Gateway Timeout',
			505 => 'HTTP Version Not Supported',
			506 => 'Variant Also Negotiates',
			507 => 'Insufficient Storage',
			510 => 'Not Extended'
		);
	return $wp_header_to_desc[$code];

}

function status_header( $header ) {
	$text = get_status_header_desc( $header );

	if ( empty( $text ) )
		return false;

	$protocol = $_SERVER["SERVER_PROTOCOL"];
	if ( 'HTTP/1.1' != $protocol && 'HTTP/1.0' != $protocol )
		$protocol = 'HTTP/1.0';
	$status_header = "$protocol $header $text";
	if ( function_exists( 'apply_filters' ) )
		$status_header = apply_filters( 'status_header', $status_header, $header, $text, $protocol );

	return @header( $status_header, true, $header );
}
?>

Caution: Destroy your session value when user will be logged out.
Note: I wrote this article very quickly , so it is very simple to have any mistake in my words. I just copied my codes and modify some variables. I have not enough time to test my modified codes. So if you found any mistake in my php codes please feel free to leave a comment with my mistake. I will update my codes.

If this article is helpful to you don’t forget to share with others.

THIS POST MAY BE THE ANSWER OF FOLLOWING QUERIES:

  • How to Protect Your Uploaded Files When User is not Logged in
  • WordPress Plugin to protect my files
  • A guide to protect my restricted files from non authorized users
  • PHP script to protect downloadable files
  • How can I hide my files from not logged in visitors
  • Lock your content from unauthorized visitors
  • Prevent unauthorized access to your restricted or private files
  • Keep your website’s uploaded files safe from unknown visitors
  • Lock your uploaded files when user not logged in
  • How to prevent unauthorized access from my server files
  • Protecting your media files from logged off users using php

Leave a Reply

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