Avatar of OmniUnlimited
OmniUnlimited
Flag for United States of America asked on

Security for Downloadables

Hello Experts!

I thought I'd tap into this great pool of genius to find out what the latest in technology is with relation to security of downloadable items.

A client of mine wants me to create a site in PHP where he can sell ebooks, videos and audio files and I wanted to know what is the best way to prevent the download of these items from off the server unless the person has paid.

Extra points to the person who can share a personal success story and how he did it.

Thanks!
PHPSecurity

Avatar of undefined
Last Comment
OmniUnlimited

8/22/2022 - Mon
gr8gonzo

I've created this setup several times. The methodology is secure and sound, so it hasn't evolved much over the past few years. The basic idea is done in 3 basic steps:

#1. Never keep the actual files in a web-accessible directory.

#2. Use a PHP script to access the file contents from outside the document root, and then stream it to the browser (combination of header() and readfile() lines - about 10-15 lines of code).

#3. Use the script to validate the user's permissions to access the requested file.

The bulk of the work is in step 3, because the "validation" varies per project. The most common and easy method is to have a database backend that gets updated whenever a user makes a purchase of an item, and the script simply checks the user ID when they're logged in to see if they purchased the item. If so, it streams the file. If not, it rejects with a message to indicate they need to purchase the item first.

If you want to be ultra-safe about it, then you generate a random token when the person buys the item. This token is good for one use. When they click on a link that passes the token to the download script, the download script first marks the token as used and then streams the file. This prevents the end user from sharing his credentials and letting others log in and copy the file, too.

However, all this does is handle the server-side permissions. Trying to prevent the file from being physically copied and distributed after it's been downloaded is a different story and that gets FAR FAR more complicated.
OmniUnlimited

ASKER
Wow gr8gonzo, what fantastic information!  To be honest with you, this is my first time, so be gentle. :)

What command would you recommend using to access a non-web accessible file?
gr8gonzo

There is a PHP command called readfile(). All it does is take in a filename as a parameter and then pushes all the data back to the browser.

Try creating a script like this to see how it works.

<?php
$file = "/path/to/some/file.txt";
$filename = basename($file);

if(file_exists($file))
{
	// Send download headers
	header('Pragma: public'); 
	header('Last-Modified: '.gmdate('D, d M Y H:i:s') . ' GMT'); 
	header('Cache-Control: no-store, no-cache, must-revalidate'); // HTTP/1.1 
	header('Cache-Control: pre-check=0, post-check=0, max-age=0'); // HTTP/1.1 
	header("Content-Transfer-Encoding: none");
	header("Content-Type: application/octetstream; name=\"" . $filename . "\""); // IE & Opera 
	header("Content-Type: application/octet-stream; name=\"" . $filename . "\""); // The rest  
	header("Content-Disposition: attachment; filename=\"" . $filename . "\"");
	header('Content-Disposition: inline; filename="' . $filename . '"'); 
	header("Content-length: " . filesize($file)); 
	
	// Send file contents
	readfile($file);
}
else
{
	echo "Invalid request";
}
?>

Open in new window

This is the best money I have ever spent. I cannot not tell you how many times these folks have saved my bacon. I learn so much from the contributors.
rwheeler23
Ray Paseur

No points for this, please, but @gr8gonzo has the right answer.  And if you want to save a lot of money and time, hire him to build it for you!
OmniUnlimited

ASKER
Wow gr8gonzo, to get an endorsement from Ray must mean you really know what you are talking about.  Course, I could get the gist of that from the response you gave to my question.

@Ray, thanks for your always succinct comments, and you are absolutely correct.  If I want the job done right, I probably should hire gr8gonzo to do this for me.  But I am a curious, and very hands on type of person.  I want to know why things work, and so the only way I can learn is by asking and by trial and error.  I'm grateful to gr8gonzo for such great help.

@gr8gonzo, your code is freaking fantastic!!!  I attempted a test on a php file I stored in a web inaccessible section of the server and your code output its contents to the screen.  How would I do it if I wanted the browser to allow the user to download it to their computer?
Marco Gasi

No points for this please. The only two differences I see between my code and the code posted by gr8gonzo are

1 - I don't use the line header('Content-Disposition: inline; filename="' . $filename . '"');
2 - I use basename: header ("Content-Disposition: attachment; filename=" . basename($file));

I'm not an expert about headers, but I suspect that Content-Disposition: inline make the file content to be printed on the screen.

Cheers
⚡ FREE TRIAL OFFER
Try out a week of full access for free.
Find out why thousands trust the EE community with their toughest problems.
Ray Paseur

If you want to force a download, this code will almost always work perfectly.  But you still need to test it on all browsers.
<?php // RAY_force_download.php
error_reporting(E_ALL);


// DEMONSTRATE HOW TO CAUSE A FILE DOWNLOAD


// REQUIRED FOR USE WITH THE PHP date() FUNCTIONS
date_default_timezone_set('America/New_York');

// A FILE TO DOWNLOAD - THIS LINK COULD COME IN THE URL VIA $_GET, OR COULD BE GENERATED INSIDE THE SCRIPT
$url = "http://www.LAPRBass.com/piechart.png";

// THE USE CASE FOR THE FUNCTION
force_download($url);



// FUNCTION TO FORCE A DOWNLOAD FROM A FILE
function force_download($filename)
{
    // GET THE CONTENTS OF THE FILE
    $filedata = file_get_contents($filename);

    if ($filedata)
    {
        // GET A NAME FOR THE FILE
        $basename = basename($filename);

        // THESE HEADERS ARE USED ON ALL BROWSERS
        header("Content-Type: application-x/force-download");
        header("Content-Disposition: attachment; filename=$basename");
        header("Content-length: ".(string)(strlen($filedata)));
        header("Expires: ".gmdate("D, d M Y H:i:s", mktime(date("H")+2, date("i"), date("s"), date("m"), date("d"), date("Y")))." GMT");
        header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");

        // THIS HEADER MUST BE OMITTED FOR IE 6+
        if (FALSE === strpos($_SERVER["HTTP_USER_AGENT"], 'MSIE '))
        {
            header("Cache-Control: no-cache, must-revalidate");
        }

        // THIS IS THE LAST HEADER
        header("Pragma: no-cache");

        // FLUSH THE HEADERS TO THE BROWSER?
        flush(); // NOT SURE THIS IS NECESSARY

        // CAPTURE THE FILE IN THE OUTPUT BUFFERS - WILL BE FLUSHED AT SCRIPT END
        ob_start();
        echo $filedata;
    }

    // ERROR
    else
    {
        die("ERROR: UNABLE TO OPEN $filename");
    }
}

Open in new window

You can wrap that script with some kind of PHP authentication, perhaps integrating a data base that contains a record showing that the client has paid for the download.  You might use a design that is sort of like the "handshake" in this article.
https://www.experts-exchange.com/Web_Development/Web_Languages-Standards/PHP/A_3939-Registration-and-Email-Confirmation-in-PHP.html

There is another issue that may or may not matter to you, and that is the issue of copyright and redistribution.  Because it is so easy to take electronic information and distribute it, enforcement of online electronic copyright is essentially impossible.  Thus we resort to shaming the thieves.  The general design pattern I've seen for this includes adding the following information to the document:  The source URL, date and time of download, the identity of the individual who performed the download, and the statement of copyright, along with a URL link to report theft and abuse.  In the case of images, this information is often put into a watermark.  With PDF documents, it's usually put into the document footer on every page.  With video files it may be in the introduction or trailer and is usually repeated at some point in a semi-transparent watermark.  I have no idea how to do it with pure audio, so I would probably just make a video.
ASKER CERTIFIED SOLUTION
gr8gonzo

THIS SOLUTION ONLY AVAILABLE TO MEMBERS.
View this solution by signing up for a free trial.
Members can start a 7-Day free trial and enjoy unlimited access to the platform.
See Pricing Options
Start Free Trial
GET A PERSONALIZED SOLUTION
Ask your own question & get feedback from real experts
Find out why thousands trust the EE community with their toughest problems.
Marco Gasi

lol @gr8gonzo: sometimes I'm really blind: I just didn't see it! :-)

Bye.
SOLUTION
Member_2_248744

THIS SOLUTION ONLY AVAILABLE TO MEMBERS.
View this solution by signing up for a free trial.
Members can start a 7-Day free trial and enjoy unlimited access to the platform.
See Pricing Options
Start Free Trial
⚡ FREE TRIAL OFFER
Try out a week of full access for free.
Find out why thousands trust the EE community with their toughest problems.
OmniUnlimited

ASKER
My apologies to everyone for the delay.  Between the holidays and some system failures, I wasn't able to respond until now.

Thank you to all who participated.  gr8gonzo, your solution is absolutely fantastic, it works like a charm.  Thank you for the code.

Here's wishing the very best and prosperous New Year to everyone!
Your help has saved me hundreds of hours of internet surfing.
fblack61