Using PHP/Apache to restrict access to static files (html, css, img, etc)

35,171

Solution 1

I would consider using a PHP loader to handle authentication and then return the files you need. For example instead of doing <img src='picture.jpg' /> Do something like <img src='load_image.php?image=picture.jpg' />.

Your image loader can verify sessions, check credentials, etc. and then decide whether or not to return the requested file to the browser. This will allow you to store all of your secure files outside of the web accessible root so nobody is going to just WGET them or browse there 'accidentally'.

Just remember to return the right headers in PHP and do something like readfile() in php and that will return the file contents to the browser.

I have used this very setup on several large scale secure website and it works like a charm.

Edit: The system I am currently building uses this method to load Javascript, Images, and Video but CSS we aren't very worried with securing.

Solution 2

X-Sendfile

There's a module for Apache (and other HTTP servers) which lets you tell the HTTP server to serve the file you specify in a header in your php code: So your php script should look like:

// 1) Check access rights code
// 2) If OK, tell Apache to serve the file
header("X-Sendfile: $filename");

2 possible problems:

  1. You need access to rewrite rules (.htaccess enabled or direct access to config files)
  2. You need mod_xsendfile module added to your Apache installed

Here's a good answer in another thread: https://stackoverflow.com/a/3731639/2088061

Solution 3

I have been thinking a lot about the same issue. I am equally unhappy with the PHP engine running for every small resource that is served out. I asked a question in the same vein a few months ago here, though with a different focus.

But I just had an awfully interesting idea that might work.

  • Maintain a directory called /sessions somewhere on your web server.

  • Whenever a user logs in, create an empty text file with the session ID in /sessions. E.g. 123456

  • In your PHP app, serve out images like this: /sessions/123456/images/test.jpg

  • In your htaccess file, have two redirect commands.

  • One that translates /sessions/123456/images/test.jpg into /sessions/123456?filename=images/test.jpg

  • A second one that catches any calls to //sessions/(.*) and checks whether the specified file exists using the -f flag. If /sessions/123456 doesn't exist, it means the user has logged out or their session has expired. In that case, Apache sends a 403 or redirects to an error page - the resource is no longer available.

That way, we have quasi-session authentication in mod_rewrite doing just one "file exists" check!

I don't have enough routine to build the mod_rewrite statements on the fly, but they should be easy enough to write. (I'm looking hopefully in your direction @Gumbo :)

Notes and caveats:

  • Expired session files would have to be deleted quickly using a cron job, unless it's possible to check for a file's mtime in .htaccess (which may well be possible).

  • The image / resource is available to any client as long as the session exists, so no 100% protection. You could maybe work around this by adding the client IP into the equation (= the file name you create) and do an additional check for %{REMOTE_ADDR}. This is advanced .htaccess mastery but I'm quite sure it's doable.

  • Resource URLs are not static, and have to be retrieved every time on log-in, so no caching.

I'm very interested in feedback on this, any shortfalls or impossibilities I may have overlooked, and any successful implementations (I don't have the time right now to set up a test myself).

Solution 4

Create a rewrite map that verifies the user's credentials and either redirects them to the appropriate resource or to an "access denied" page.

Solution 5

Maintaining the contents of the htaccess files looks to be a nightmare. Also, your stated objective is to prevent non-authenticated users not non-authenticated client ip addresses from accessing this content - so the approach is not fit for purpose:

Multiple users can appear to come from the same IP address

A single users session may appear to come from multiple addresses.

I worry that my first solution could get pretty expensive on the server as the number of users and files they are accessing increases

If you want to prevent your content from leaching and don't want to use HTTP authenitcation then wrapping all file access in an additional layer of logic is the only sensible choice. Also, you don't know that using PHP for this is a problem - have you tested it? I think you'd be surprised just how much throughput it could deliver, particularly if you use an opcode cache.

I'm guessing your 'simplification' of the wrapper addresses issues like mime type and caching.

C.

Share:
35,171
shrooma
Author by

shrooma

Making teh internets...

Updated on September 19, 2020

Comments

  • shrooma
    shrooma over 3 years

    Lets say you have lots of html, css, js, img and etc files within a directory on your server. Normally, any user in internet-land could access those files by simply typing in the full URL like so: http://example.com/static-files/sub/index.html

    Now, what if you only want authorized users to be able to load those files? For this example, lets say your users log in first from a URL like this: http://example.com/login.php

    How would you allow the logged in user to view the index.html file (or any of the files under "static-files"), but restrict the file to everyone else?

    I have come up with two possible solutions thus far:

    Solution 1
    Create the following .htaccess file under "static-files":

    Options +FollowSymLinks  
    RewriteEngine on  
    RewriteRule ^(.*)$ ../authorize.php?file=$1 [NC]
    

    And then in authorize.php...

    if (isLoggedInUser()) readfile('static-files/'.$_REQUEST['file']);
    else echo 'denied';
    

    This authorize.php file is grossly over simplified, but you get the idea.

    Solution 2
    Create the following .htaccess file under "static-files":

    Order Deny,Allow
    Deny from all
    Allow from 000.000.000.000
    

    And then my login page could append that .htaccess file with an IP for each user that logs in. Obviously this would also need to have some kind of cleanup routine to purge out old or no longer used IPs.


    I worry that my first solution could get pretty expensive on the server as the number of users and files they are accessing increases. I think my second solution would be much less expensive, but is also less secure due to IP spoofing and etc. I also worry that writing these IP addresses to the htaccess file could become a bottleneck of the application if there are many simultaneous users.

    Which of these solutions sounds better, and why? Alternatively, can you think of a completely different solution that would be better than either of these?

  • shrooma
    shrooma over 14 years
    Thanks Shane. This loader is basically what I describe in my first solution. My concern with it is the server expense, but I am glad to hear that it worked well for you in your "large scale secure website".
  • Shane
    Shane over 14 years
    I don't typically use the .htaccess rules you mentioned above (though I see no reason not to) I just create some wrapper functions that build the correct query strings and pass arguments. Since your server typically serves back binary data anyway the only additional overhead is PHP processing which (at least to me) is well worth the security you gain especially vs the .htaccess overhead of having it check many (hundereds|thousands) of ip addresses for EVERY page request in the system. Just one of many ways to skin this cat I think! Let me know if you come across something else!
  • shrooma
    shrooma about 14 years
    Yes! I played a little bit with this same idea after originally writing this question. A RewriteMap were it rewrites to the originally requested URL if authenticated and a denied page if not authenticated. For the life of me I couldn't get it working. I couldn't even get Apache to start up when specify a PHP script in PRG. Do you have any examples or tips for using this type of technique? My setup is: Apache 2.2 / PHP 5.2 / Windows Server 2008
  • Ignacio Vazquez-Abrams
    Ignacio Vazquez-Abrams about 14 years
    The script needs to be executable which on Windows means that .php has to be associated with the PHP CLI executable. Also, the script should call flush() upon output, and should loop back and wait for further input; the script is started on httpd startup and is killed on httpd shutdown.
  • Pekka
    Pekka about 14 years
    PHP is definitely a problem for this on heavy-duty sites with many calls. It's not just the putting through the data; the whole user authentication process has to be performed for every single small resource. This usually weighs a few MBs or RAM, and that each time per requested resource.
  • symcbean
    symcbean about 14 years
    define "heavy duty"? I've had much more complex scripts processing half a million hits per day on a 1ghz celeron with 512 Mb of memory (OK, actually 4 times that volume on 4 servers) pulling in data from a DB and still giving response times averaging 250 ms.
  • shrooma
    shrooma about 14 years
    No I haven't tested it yet. But regardless of the details, a web server simply serving a file has got to be less expensive than PHP running, performing some filesystem functions like is_file and stat, and then finally serving the file via readfile.
  • Lotus Notes
    Lotus Notes almost 13 years
    Is it correct to say that none of the images would be able to be cached by the browser in this case?
  • Shane
    Shane over 12 years
    Not necessarily. It is actually possible to set your own headers (including cache handling headers) in PHP. You can also check the headers sent to you from the browser to determine your response. It's a little more complicated than some folks like to get into, but man you have some control then. I do send cache control headers back so I can have very tight control of my caching.
  • Tchoupi
    Tchoupi over 11 years
    That doesn't completely answer the question since the OP needs those files to be accessible somehow by authorized users.
  • Andras Balázs Lajtha
    Andras Balázs Lajtha over 11 years
    I think that if any single small resource is protected than the number of auth script requests can be ten or hundred fold the number of real requests. But do you really want to protect the white 6*78px that acts as a spacer in a poorly designed website? If you protect only the content, the authentication for the image the user wants to load will only double the burden on the server.
  • Michel
    Michel about 9 years
    @Shanee, how can you also prevent a user to just load mysite.com/picture.jpg? I know the use has to know the file and path names to do this, but is it also possible to prevent this?
  • Shane
    Shane about 9 years
    @michael - you need to store you media outside of the web accessible root. Doing that will mean that anyone on the outside can't directly access the image. The PHP handler file can then find the correct file on the server (outside of web root) and serve it back if sessions and security checks are approved.
  • mathieu
    mathieu about 8 years
    This is definitely the best solution to this problem.