Restrict specific folders from public download (via .htaccess)

Last modified: November 26, 2009 - 21:41

If you set "public" as the download method, you can still protect some of your folders by settings in your .htaccess file, if you have mod_rewrite enabled.

For instance, if your files live in sites/default/files, and you want to protect everything in sites/default/files/protected_download_dir, then you can add the following line to your central .htaccess file:

RewriteRule ^sites\/default\/files\/(protected_download_dir\/.*)$ index.php?q=system/files/$1

The files in this folder (or, all files that match the regular expression) will not be served directly by apache, but by a full drupal request using the file_download() callback. The routing for system/files is defined in system_menu().

.htaccess troubleshooting

There are some pitfalls when writing the htaccess file: you need to write correct regex syntax, you need linebreaks between different rules, you might need specific RewriteCond statements with each rule, you need the correct apache configuration, etc.

If you have something to share about your htaccess configuration, please do so!

Access Checking for File Downloads

Modules can do access checking on files downloaded through file_download(), by implementing hook_file_download().

For instance, the filefield module will do access checking for all files that you upload via filefield. It will only give access to people who are allowed to see the node and the field the file belongs to.

In addition, you can look for modules that restrict file downloads based on the folder name or other criteria.

Access Checking built-in with Filefield

The filefield module has access checking built-in. To make use of this, please

  1. Make sure to use the latest dev version of filefield (for now). The current offical release (08/2009) has a bug in access checking. Have a look into filefield issue 516104.
  2. Configure the filefield you want to protect. Use the filefield path module, to define where the files for the filefield should be stored by default.
  3. Add a line to your .htaccess to protect this folder (see above).
  4. Use whatever modules to restrict node or field access.

Files Attached to Nodes

If you want to protect files uploaded with the core upload module, have a look at the Private Upload module.

Folder-specific Permissions

As an alternative, you can restrict folder access based on the Drupal permission system, introducing one permission per folder. Have a look at

Typical Slip-Through Candidates

If you want to protect your files, you should think of the following situations where a file might remain unprotected:

  • Thumbnail images (for instance, those generated with imagecache or imagefield) typically live in a different directory than the original. If you want to protect these as well, you need to add new rules to your .htaccess file. You might even have to install additional modules for the access checking.
  • Import directories. If you use an importer to add your files, it can happen that a copy of the file is still in the import directory. Solution is to either manually clean up the import directory (via FTP), or to protect it (using a .htaccess rule).
  • Orphan files. Sometimes when you delete a node with a filefield, the file will still be there, but it will no longer be associated with any filefield node. This can happen if your filesystem permissions don't allow deleting files. As a consequence, filefield's access checking will no longer apply to this file. You need to find (or code) a new module to protect these files, or you have to clean up manually.

Many thanks for this, been

giorgio79 - August 20, 2009 - 13:29

Many thanks for this, been looking for ages on how to prevent hotlinking to private files!

This solution works nice with CCK filefield, and the defauls CCK permissions module that is now bundled with CCK.

Much appreciated.

Position in .htaccess

psykocybernetik - August 23, 2009 - 21:53

Hi,
I have tried to add the file in the .htaccess but I received an error 500. Where exactly does the line should go in the file?
Thanks for your help.

Look into your error.log

donquixote - August 24, 2009 - 12:36

Please, can you have a look into your error.log, and try to give a more detailed report?

Where it goes: In the default D6 .htaccess there is a section beginning with <IfModule mod_rewrite.c>. You can add the line anywhere in that section, but you might have to edit it a bit.

Troubleshooting:
0. Make sure the file and directory exist.
1. Your site should have URL rewriting enabled.
2. Your site's download method should be set to "public".
3. Make sure the RewriteBase in your .htaccess is pointing to the right directory.
4. Apache (httpd.conf) should have mod_rewrite enabled.
5. Some apache and mod_rewrite problems can be caused by having the wrong settings for FollowSymLinks and AllowOverride in httpd.conf. Try a google search or this discussion.

If that all doesn't help you could do some debugging, and check if your index.php filed is ever called ..

.htaccess not enough

psykocybernetik - August 24, 2009 - 21:34

Thanks a lot for your answer. I think I made an error when I add the line in the .htaccess file but I haved corrected it.

What I didn't realized at first with this method is that modifying the .htaccess is not the only thing to do to restrict the access to the file. It is mentioned that one should re-implement some hooks but since I am a drupal beginner I didn't realized that it meant creating a module.

By combining this page with this one: http://www.drupalcoder.com/story/406-mixing-private-and-public-downloads..., I have now been able to restrict access to the private folder.

Thanks a lot for your help.

Some info was missing in the

donquixote - August 24, 2009 - 22:32

Some info was missing in the book page - thanks for pointing this out. I linked the drupalcoder article.

You can implement hook_file_download with a custom module, but this is not necessary if you work with filefield or use one of the other existing modules.

If you want a custom module doing the access checking, you have the choice to either implement hook_file_download, or set a permission in hook_menu (you can google the hook names to get more info).

Am I missing something?

jaybee1001 - November 26, 2009 - 17:33

I'm using (Acquia) Drupal, FileField 6.x-3.2 (with fix for issue 516104) plus the CCK field permissions configured to allow certain roles view access. However, as an anonymous user I can still hotlink to the file and download it...

In my main htaccess I have:

RewriteRule ^sites\/mysite.localhost\/files\/subscribers\/(pdfs\/.*)$ /index.php?q=system/files/$1
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !=/favicon.ico
RewriteRule ^(.*)$ index.php?q=$1 [L,QSA]

Should this all now be working and preventing anonymous access?

Please Help!!!

Two things you can try: - Put

donquixote - November 26, 2009 - 17:38

Two things you can try:
- Put an empty line between different rewrite rules.
- instead of /index.php?q=system/files/$1, make it /index.php?q=system/files/subscribers/$1

I'm not sure if this will help. If not, ask again.

No joy...

jaybee1001 - November 26, 2009 - 17:50

Thanks for responding so quickly.

Tried your suggestions, but files still appear unprotected.

Any other ideas?

You need to find out if the

donquixote - November 26, 2009 - 18:22

You need to find out if the request does go through index.php (in which case the problem is with the access checking), or if it's a direct file download. And then, you need to check which menu callback is used: What you want is file_download().

The quick and brutal way to check this (if you are not on a live site) is to put echo and die() statements in the respective places: In index.php, and in the file_download() function.

Sorry to be...

jaybee1001 - November 26, 2009 - 18:29

...dense, but do you mean a random echo statement in index.php and a die() statement in file_download()?

I mean the following: First,

donquixote - November 26, 2009 - 18:54

I mean the following:
First, in index.php, you write

<?php
// very beginning of the file.
echo 'This is index.php'; die;

// and here is the rest of the file. It is never executed.
?>

Try to download the protected file, and see what happens. This will tell you if index.php is called.

Next, you remove the debug stuff from index.php, and instead put it some debug stuff in file_download();

<?php
function file_download() {
  echo
'I am the file_download function'; die;
}
?>

Try again to download the file, and see what happens.
Don't forget to remove the debug stuff afterwards.

So, what I know now is

jaybee1001 - November 26, 2009 - 20:03

that the rewrite rule isn't working, as the hacked index.php isn't called when addressing the file's url.

I checked my file system settings in drupal, and they point to: sites/mysite.localhost/files

so I re-wrote the re-write as:

RewriteRule ^sites\/mysite.localhost\/files\/subscribers\/(pdfs\/.*)$ index.php?q=system/subscribers/pdfs/$1

changing q=system/files... to q=system/subscribers/pdfs/$1

Still no joy....

Then I tried a bunch of other q= settings, but nothing - which leads me to think that the rewrite isn't being triggered.

Does it need a RewriteCond?
What is the value of the $1 parameter?

Any clues?

And thanks for sticking with this for me!

UPDATE: A troubleshooting

donquixote - November 26, 2009 - 21:32

UPDATE:
A troubleshooting chat revealed that it's a .htaccess issue: The index.php was never called.
One quite obvious bug is the unescaped "." in "mysite.localhost" - but fixing this didn't solve the problem.

Therefore:
Everyone is invited to post working htaccess solutions, and information about typical problems and how to solve them! Useful information will be added to the article.
Thanks!

DOH!

jaybee1001 - November 27, 2009 - 17:57

It was a typo in the directory name on-disk - so the regex was never matching. Thanks donquixote for all your patience and help.

 
 

Drupal is a registered trademark of Dries Buytaert.