Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations sizbut on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

Protecting media files 1

Status
Not open for further replies.

OsakaWebbie

Programmer
Feb 11, 2003
628
JP
I have a private (small number of users) PHP-MySQL site that uses session authentication, and I want to offer MP3 files to be accessed by users only (I'll put them on pages via embed tags with mini-controls that the users can play). But how do I keep the media files themselves from being accessed directly by non-authenticated people? I found a few others who have asked this question (or something similar), but I didn't see an answer that completely satisfied me. These are three suggestions I saw:

1) Use HTTP authentication (.htaccess) on the directory where the media files would be - I haven't seen anyone suggest how to avoid the users having to log in twice, and me having to maintain two lists of users/passwords (DB and htpassword).

2) Place the files outside the web tree and use passthru or readfile to serve them - as far as I can tell, that would not allow me to embed the audio on a page with HTML, but only to serve the binary file directly with the appropriate Content-Type - not what I want.

3) Keep the permanent files outside the web tree and make a randomly named copy in the web tree when needed - pretty cumbersome, especially regarding deciding when and how to delete the temporary files later.

Are there other, simple but effective ideas? Or ways to overcome the problems in one of the above methods?
 
In scenario 2, what do you mean by "embed the audio on a page with HTML"? If you're talking about something like a background sound on a page, then you're not talking about anything different from an <A> link to the file.



Want the best answers? Ask the best questions! TANSTAAFL!
 
Yes, the <embed> tag makes it background music (although I will allow my users some control over playing it - you can have a "console" with some controls). The <a> link tag is different from that - clicking on it would make the file play in a standalone player, rather than right on the page. But anyway, my question would be valid with either method. To embed or link, I have to put the MP3 file somewhere in my web tree, which means that if someone other than one of my users got ahold of the HTML of one of my pages (or stumbled upon the directory by luck), they could download the audio files by typing the path directly in the address line of their browser. The files are copyrighted music - I am only offering them as a way for my users to learn the songs (we are a worship team for a church). So I would prefer not allowing the world even theoretical access to them.
 
To the best of my knowledge, whether you use an <a> tag or an <embed> tag, the file must go through the browser and so you have your password system working.



Want the best answers? Ask the best questions! TANSTAAFL!
 
Using a URL such as sure, my PHP code authenticates the user, so if the requester is not logged in, my code will give him a login form, rather than the info about the song and a link to the audio file. But what if I decide to name my audio file directory "audio" and my files by the song number, and somehow someone learns those facts? (Not hard to do, since I'm not worried enough about my application to run it through SSL.) Since my authentication is by PHP code, not a .htaccess file, then there is nothing stopping him from typing in his browser and downloading the file.

That's my question, and I'm not the first one to ask it, but I didn't find any easy answers elsewhere. A few years ago I even programmed a solution, when working for a company who wanted very high security on files they would allow registered users of their software to download - I used method three as explained in my first post, but it was complex to implement, and required direct access to server files I don't think I can get to on a shared commercial hosting service server, such as crontabs. I don't need this as airtight as their requirements were, but I think it's prudent to protect copyrighted material a little bit - I was hoping for something fairly easy. But please don't tell me that the files are secure already just because I don't have public links to them - "security by obscurity" is not very secure at all.
 
OsakaWebbie said:
But what if I decide to name my audio file directory "audio" and my files by the song number
The important thing to remember is that your authentication system must at all times stand between your users and your media.

With .htaccess files, it's pretty-much automagic. Using PHP for security, however, it's important to make sure that the only way to get to the media is through your PHP scripts. This is where solution 2 comes in -- store your media outside the web site's document root and let a PHP script stream the files. PHP's filesystem functions are not contrained to the document root of a web site, but rather can operate on the entire filesystem.



Want the best answers? Ask the best questions! TANSTAAFL!
 
if, as with some of my hosts, i cannot store files outside the document route, i would use method 1.

the users would need to login against the WEBSERVER if they wanted direct access to a music resource stored in an htaccess protected folder.

on your main page:

1. use sessions
2. authenticate incoming users with your normal pwd protection
3. for each <embed> set the src attribute to a php file in the unprotected directory (as sleipnir214 suggests)
4. have the embed.php file check for HTTP_REFERER (for some reason this is spelled incorrectly in php) and reject any direct attempts at access.
5. the embed.php file then uses fopen +fread/fpassthru on the actual file in the protected directory. as this is filesystem rather than served there is no problem (as sleipnir214 also says).

I believe there are ways to fool HTTP_REFERER but i can't think of a better way at the moment.

there are many other hindrances you can put in place (foremost in my mind is having the songid be a uniqid and dynamic id whose validity lasts only a few minutes; another might be to create the file stream through an ajax interaction so that an examination of the source code would not reveal the url at all [but browsers like firefox would still allow users to traverse the DOM's current state so this is far from perfect])

Code:
<embed src="music.php?songid=12345" />

and music php would look something like this
Code:
session_start();
$correctreferer = "";//insert name or proper page
if (!loggedin()) die();
if (!isset($_GET['songid'])) die();
if (!isset($_SERVER['HTTP_REFERER'])) die();
if ($_SERVER['HTTP_REFERER'] !== $correctreferer) die();
//do lookup of songid to file
//or just pull the song wholesale from a database blob
$songfile = "";

if (!is_file("audio/$songfile")) die();
if (!is_readable("audio/$songfile")) die();
$fh = fopen("audio/$songfile", "rb") or die();
//you might want to set content disposition headers here
fpassthru($fh);
fclose($fh);
//log file access for royalty statements.
 
I didn't realize that the <embed> source could point to a file other than a media file - if that works, it's a great idea. I don't think I even have to use the HTTP_REFERER; I could simply use some of the same code I use throughout my site for authentication - just the part that gets login info from the session and checks it against the database. (If there is no valid login, rather than send a login form, it would just do nothing or send a 404 or something.) It's very late here now, but I'll try that implementation in the morning and let you know how it goes.

As for the "other hindrances" you mentioned, I think they're more than I need for this - the dynamic ID idea is basically my "method #3", and I don't know about AJAX yet (it seems about once a month that someone is coming out with a new language or protocol I am supposed to learn!).
 
ajax is not a language or protocol. it is just a way of glueing together a back-end process (like php or ruby etc) and a browser through the use of javascript.

of course, if you don't check the referer then it could be that your own users can download the songs in breach of copyright.

i tested the code i posted above and it does need the headers to be set in the code. here is a brief code snip for a page that self-serves. you'd want to put the protections in that i posted above too.

Code:
<?
if (isset($_GET['songid'])): 
$file = "path/to/file.mp3";
$size = filesize($file);
header("Content-Type: audio/mpeg");
header("Content-Transfer-Encoding: binary");
header("Content-Length: ".$size);
@readfile($file);
exit();
else:
?>
<embed loop=false volume=100 autostart=true src="<?=$_SERVER['PHP_SELF']?>?songid=1" hidden=false />
<? endif; ?>
 
Okay, after a night's sleep and a whole lot of debugging (I had some mysteries I never did solve but just worked around), I got this working on Firefox, but for some reason IE doesn't like it.

Self-serving is a slick idea, but in my case there are two pages that include the audio (and there might be more later), so I made it a separate file and checked for both referrers. My final solution looks like this (some filenames and variable names changed to protect the guilty):
Code:
<?php
session_start();
$comp1 = $_SESSION['urlpath']."mainfile1.php";
$comp2 = $_SESSION['urlpath']."mainfile2.php";

if (!isset($_SESSION['userid'])) exit();
if (!isset($_SERVER['HTTP_REFERER'])) exit();
$refer_noargs = $_SERVER['HTTP_REFERER'];
if (strpos($refer_noargs,"?"))  $refer_noargs = substr($refer_noargs, 0, strpos($refer_noargs,"?"));
$refer_noargs = ereg_replace("[URL unfurl="true"]http://www.","http://",$refer_noargs);[/URL]
if (!($refer_noargs == $comp1) && !($refer_noargs == $comp2)) exit();
if (!isset($_GET['songnum'])) exit();

$file = "audio/".$_GET['songnum'].".mp3";
if (!file_exists($file)) exit();
$size = filesize($file);
header("Content-Type: audio/mpeg");
header("Content-Transfer-Encoding: binary");
header("Content-Length: ".$size);
@readfile($file);
?>
I know the code could be simpler - combined statements and less variables - but I was getting some strange syntax errors that I never did find, and breaking things apart solved them, so I just left it that way. The session variable "urlpath" is there because I have this application installed on multiple servers - I have a record in the database that I read at login time that gives the path, so that I don't have to hardcode it anywhere.

I put an .htaccess file in the "audio" directory with a single, all-encompassing line: "deny from all". That seems to do the trick. All of this works if I'm looking at it in Firefox.

But when I use it in IE, in the embedded mode it gives me the box that looks like an image that is missing (and thinks the whole page loaded very quickly, so I don't think the audio data came through). If I use a regular link that opens in a new window (which was handy for debugging), it takes time to download, so it apparently is getting the audio data, but then it shows me a Quicktime logo with a crack in it, apparently symbolizing a bad audio file?

An earlier version, when some of the if statements were commented out for debugging, did work in IE, so I don't know what happened. Anyone see the problem?
 
in the embed tag turn autostart to false.
the controls don't appear when the page can't get the right file. i suspect it's because of some tests in the script being failed. this can happen, for example, if the session cookie is not getting thru from ie or do you need to add <embed> to your php url rewrite or add the SID manually)

try removing the lines

Code:
$refer_noargs = $_SERVER['HTTP_REFERER'];
if (strpos($refer_noargs,"?"))  $refer_noargs = substr($refer_noargs, 0, strpos($refer_noargs,"?"));
$refer_noargs = ereg_replace("[URL unfurl="true"]http://www.","http://",$refer_noargs);[/URL]
if (!($refer_noargs == $comp1) && !($refer_noargs == $comp2)) exit();

you might also check that the tests are being passed and instead of exiting (which will make the box look blank) you could divert the track to a different mp3.

btw there are much easier ways to parse the url than with ereg (parse_url for example)

Code:
$array = parse_url($_SERVER['HTTP_REFERER']);
if ($comp1 !== $array['host'].$array['path'] && $comp2 !== $array['host'].$array['path']) exit(); //or divert to test.mp3
 
My autostart is already false. And the session cookie is fine - I know this because much of the trouble I was having in getting the code working revolved around the check for the referrer, and when I had lots of debugging code telling me what was going on (strings in the exit functions), all the session stuff was getting through just fine. And the rest of my application would not function at all if the SID wasn't getting through - every page would be asking me to login.

I don't know what you mean by "do you need to add <embed> to your php url rewrite" - can you explain that? In case it's helpful, here is what I have in the calling pages:
Code:
  echo "<table border=0 cellspacing=0 cellpadding=0><tr><td valign=middle><b>Audio for learning:&nbsp;</b></td>";
  echo "<td valign=middle>";
//echo "<a href=sendaudio.php?songnum=".$songnum." target=_blank>Click to Listen</a>";
  echo "<embed src=\"sendaudio.php?songnum=".$songnum."\" controls=\"console\" width=\"144\" height=\"60\""; 
  echo " vspace=\"0\" hspace=\"0\" border=\"2\" align=\"top\" autoplay=false";
  echo " pluginspage=\"[URL unfurl="true"]http://www.apple.com/quicktime/download/?video/quicktime\"></embed>\n";[/URL]
  echo "</td></tr></table>";
When I'm debugging uncomment the plain link and comment out the three lines below it. Thanks for the help.
 
php handles sessions in one of two ways (by default); cookie and url rewriting. ie. if the cookie does not work because a user has turned off cookies, then PHP will rewrite certain urls within tags (such as <a>, <frame> etc) to include the SID in the url or (for forms, include a hidden element). I do not think that <embed> is included in this (for example the php.ini i am using in my installations is the following:

Code:
url_rewriter.tags = "a=href,area=href,frame=src,input=src,form=fakeentry"

to cause php to rewrite for this you would need to add "
Code:
embed=src

it may just be that the cookie is not getting through from IE whereas it is from firefox as this is handled at the browser level. this would probably not be the case on every other page as php would be able to rewrite the urls for you.

this type of session passing is called trans SID by php. by default it is turned off (at least in php5) as there are some security ramifications.

did the controls reappear once you removed the http_referer check?
 
Yes, the controls reappear as long as I remove BOTH checks on HTTP_REFERER, but it's clearly not because of sessions/cookies. First of all, I do not need to remove the check on userid, which also depends on sessions. Secondly, the first of the two offending checks doesn't have anything to do with sessions - it is merely checking to see if HTTP_REFERER is null - but yet it must be removed for IE to work.

In my debugging mode, where I replace the <embed> with a simple <a> and put informative text in all the exit() calls so that it tells me if a check fails and why, I get the following behavior:
* If either HTTP_REFERER check is active, it first downloads the page for several seconds with nothing displayed in the window, then it shows a large blue "Q" (Quicktime) icon for just a moment, and then replaces it with a different icon, of a filmstrip with a torn corner and a Q in the middle, apparently indicating data that the Quicktime plugin can't open for some reason. None of my diagnostics messages appear, which is additional evidence that all the checks evaluate to true.
* If I comment out both HTTP_REFERER checks, it first downloads the page for several seconds with nothing displayed in the window, then it shows a large blue "Q" (Quicktime) icon for just a moment, and then replaces it with the console and starts playing the file (since without the embed tag, the autostart=false isn't there).

If I use the <embed> tag, the failure conditions are the same, although the visual cues when it fails are not as informative. With both HTTP_REFERER checks commented out, the console displays and the file downloads (I can see the progress bar in the console advance). With either of the checks active, I get a rectangle with the little icon in the top left corner (with three geometric shapes) that IE gives me when an image file reference is wrong.

I tried viewing source (when using the <a> method, of course) to see if at least the headers look good, but the strange results I got are not informative. In IE, if I wait until the page is finished downloading, the View Source menu item is greyed out. If I take action quickly, before the media data is done downloading, I can view source, but both when it will fail and when it will succeed I get the same thing, which bears no resemblence to what I am actually sending - it apparently starts out with spacefiller HTML of its own invention until it gets something else. Firefox, whether before or after the file is done downloading, simply hangs when I ask for source on a big binary file - I am retyping this post because I lost it the first time (argh!).

This is quite strange. As unlikely as it might seem, something about accessing the HTTP_REFERER variable (or something else unique to those two if statements) earlier in the code apparently causes data sent by actions later in the code to get messed up. The checks all evaluate true, and the code that sends the header and gets the media file runs, but something goes awry (and remember, this is only in IE - Firefox runs fine). At one point I wondered if something was sent to the browser too early, causing a default header to be sent prematurely, but if that were true, two things would happen: it would complain about the header when the second one came along, and the document type would end up text/html, not mpeg - IE correctly invokes the Quicktime plugin, so the correct header is being recognized. Weird!
 
do some checks to see whether http_referer is being correctly sent by your browser to the server. not all browsers send it and in some the results are unpredicable.

as you point out - the session issue was a likely second fiddle to the HTTP_REFERER test!
 
Okay, even though I went through this thoroughly before (because I had some trouble dealing with cleaning up the referer value so that it would compare successfully), just for an absolutely clean test I put the following right at the top of my code:
Code:
echo $_SERVER['HTTP_REFERER'];
exit;
The answer was (path and variable munged, but you get the idea):
Code:
[URL unfurl="true"]http://www.mydomain.org/directory/mainfile1.php?songnum=266[/URL]
So that doesn't not appear to be a problem.

btw - like your post ...
Thanks - just trying to help when I am able. It's rare when I am able to participate in answers, rather than just being on the receiving side, because many of you guys answer the easy ones almost instantaneously.
 
could you test with replacing the validation tests with the following, please? this cuts out all the path info and forces the referer into lower case. just trying to go back to basics here. btw the code works fine for me on ie and firefox.

Code:
$okplaces = array ("mainfile1.php", "mainfile2.php");
$referer = parse_url($_SERVER['HTTP_REFERER']);
$referer = pathinfo($referer['path']);
$referer = strtolower($referer['basename']);

if (!in_array($referer, $okplaces)):
 exit(); //or divert to test.mp3
endif;
 
I added your code, but I get the same error - in Firefox it works fine, but in IE I get the torn filmstrip icon after the media data downloads.

This may not be worth fighting - perhaps there is a way to avoid the issue instead. I have been noticing that I don't like the delay that happens when loading the page with a working <embed> tag in it - even though most of the media is downloaded after the page is visible, the initial stuff (perhaps loading the plugin?) is still annoyingly slow compared to how it used to be. Most of the time that users are accessing the pages it is not to listen to the audio but to do other things with the song database, so it's a waste to always load the audio. So I'm thinking of making a standalone page with just the embed and the song lyrics (people will want to watch the lyrics as they listen), that self-serves to get the media file, like you suggested earlier. The main pages can then just link to that page, sending the relevent song number. If I do that, I only need to make sure the readfile section of the code can only run when the file is called by itself via the <embed>'s "src" property (I won't care where the page is called from initially, because it would still include my standard PHP authentication - my users can even bookmark it if they want). Is there a relatively simple way to do that without using the HTTP_REFERER variable?
 
If I do that, I only need to make sure the readfile section of the code can only run when the file is called by itself via the <embed>'s "src" property (I won't care where the page is called from initially, because it would still include my standard PHP authentication - my users can even bookmark it if they want). Is there a relatively simple way to do that without using the HTTP_REFERER variable?

if you are asking whether the server can be certain that a page is not called statically but only ever by an embed tag. the answer is no. there is no way i know of to do this.
 
Status
Not open for further replies.

Part and Inventory Search

Sponsor

Back
Top