Easy Protection of File Downloads in WordPress


I recently wanted to protect some files from unauthorised download on a WordPress site, but still allow authorised users to easily access to the files.

The simplest solution I found was to put the files in custom directory, place the links to the files on a WordPress password protected page, and use a .htaccess file to limit access to the files to users who are logged in. This rather simple approach works rather well if you take a little care with the directory and/or file naming.

Here is the step-by-step guide.

1. Make a new directory on your site and upload the files you want to protect to this directory (using ftp or scp). Make sure you chose a directory name that is hard to guess. I would recommend a random string — something like “vg4thbspthdbd8th” — just don’t use this exact string!

mkdir /path_to_protected_directory/

2. ssh into the server and and create a .htaccess file in the protected directory using nano.

sudo nano /path_to_protected_directory/.htaccess

3. Copy and paste the following text into the .htaccess file.

Options -Indexes
php_flag engine off
RewriteEngine on
RewriteCond %{HTTP_REFERER} !^http(s)?://(www\.)?yourwebsite.com [NC]
RewriteCond %{HTTP_COOKIE} !^.*wp-postpass.*$ [NC]
RewriteRule \.(zip|rar|exe|gz)$ - [NC,F,L]

4. Change the yourwebsite.com to your website’s actual name. You should also change the RewriteRule line to suit the content you wish to protect. Just add the extensions of any file type you want to protect from unauthorised download.

That is it.

The major limitation with this approach is the download protection depends on the content of the user’s cookies. Since these can be faked by the technically knowledgeable, the protection is not perfect.

This is not as big a problem as it might first appear, because as long as you give the files and/or the directory non-obvious names, any unauthorised user will not know the required path to the files. They will only know the correct paths if they can log in, and if they can do this, they don’t need to fake any cookies.

While not perfect, this approach should work well for the casual protection of unauthorised downloads, but don’t use it for very sensitive files!

(104)Connection reset by peer: mod_fcgid: error reading data from FastCGI server

This error (104)Connection reset by peer: mod_fcgid: error reading data from FastCGI server started to appear in my apache error log whenever I tried to upload a file. Apache seemed to be working fine with static html pages, it was only when I tried to upload files that I had problems.

Searching for a solution suggested a whole series of possible causes all revolving around permission problems. Since I had not changed anything on the server for a few weeks, and everything looked fine permission wise, this did not look to be the cause. After checking everything I could think of I noticed that the disk quota allocated by Virtualmin was only 1 GB and that I had used nearly all of it. I upped the disk quota to 10 GB and the problem was fixed. I hope this helps someone.

pear.php.net is using a unsupported protocol – This should never happen.

I ran into the error “pear.php.net is using a unsupported protocol – This should never happen.” when I tried to upgrade my pecl packages. I have to say errors like “this should never happen” shouldn’t happen, but since it obviously did what is the cause and more importantly the solution. Apparently this error is caused because PHP 5.2.9 and 5.2.10 were broken and it corrupted the .record folder. The solution to fix this is to upgrade pear first then pecl.

pear upgrade
pecl upgrade

So simple :)

How to set cookies using PHP and get them via JavaScript

I had an application where I wanted to set a cookie value using the php setcookie function and then later pull out the value using JS. The problem is the php setcookie function url encodes the cookie values with ‘+’ symbols for any whitespace. When you later pull out the cookie value using the JS decodeURIComponent function the ‘+’ symbols remain, converting a string to like ‘abc cdf’ to ‘abc+cdf’. If you want to the cookie values to have ” ” rather than “+” then you need to str_replace all the “+” with “%20” after calling the php urlencode function and then save the encode string using setrawcookie. The basic call to do something this is:

setrawcookie("cookie", str_replace('+','%20',urlencode($string)), time() + 3600*24*7, "/path","www.yourwebsite.com");

Hopefully this will save someone some time. If you want to do the reverse it is very simple – just calling the php urldecode function on the server $_COOKIE variable.

$string = urldecode($_COOKIE['string']);

Of course all the usual warnings about not trusting user supplied data remain. Just because your script created the data does not mean you can trust what is actually there.

Root privilege scripts from Apache

If you have a script that needs to access functions that can only be run as root (e.g. chmod, chgrp, mkdir, etc) you will find that you can’t call these directly since the Apache user is not root (at least it should not be root). There is no perfect solution around this as all solutions involve some security risk, but the least bad seems to be to use sudoer to grant root privileges to the script and then lock down the script so nobody other than root can modify the script.

First chmod the script so that anyone can execute it, but nobody other than root can modify it (I am assuming here that you are logged in as root, otherwise sudo).

chmod 111 /home/path_to_script

Next modify sudoer using visudo. It is a good idea to use visudo so that any change you make are updated without having to restart sudo.

# visudo

Add the following line after the root entry in sudoer

apache_user ALL = NOPASSWD: /home/path_to_script

Change the apache_user to whatever your apache user is (e.g. nobody) and then add the path to your script. You might want to add your favorite editor (mine is nano) to your export in .bashrc. You should now be able to call your script from apache without problem.

Update. Make sure that you have commented out the Defaults requiretty line in visudo or else the script won’t be run by Apache. This problem wasted a couple of hours of my time since the script would run fine from the bash shell of the apache user, but not when called by apache. I finally took a look at the log file (yes I should have done this first) and there was the problem sudo: sorry, you must have a tty to run sudo!

Random PHP string

I needed to generate a random string in php that did not repeat. This little snippet will generate a random non-repeating 32 character string.

// Generate random 32 character string
$string = md5(time());

PHP number_format function problem

I have been using the php number_function() to format currency values in my code and was recently caught by the thousand separator comma. If you use the function and just specify the the number of decimal places (ie $x = number_format ($y,2) ) then you will get a comma introduced for all numbers above 999.99. This comma will break any maths functions that you might use downstream.

The solution is to specify the decimal point but remove the comma. The format to use to achieve this is $x = $number_format ($y,2,”.”,””) – the empty double quotes tell the number format function to not include the thousand separator comma.