DEV Community

Cover image for Your database dumps are protected by an .htaccess. Your server may not read it.

Your database dumps are protected by an .htaccess. Your server may not read it.

We ran an audit of our own shop this summer. Three .sql files were sitting in the web root, downloadable by anyone who typed the right URL. Nobody had been careless in any dramatic way. They were the ordinary residue of ordinary work: an export taken before an upgrade, a copy made to test something locally, a dump someone pulled through a hosting panel and never came back for.

That is the honest version of how this happens, and it is worth writing down, because the usual response — "PrestaShop protects the backup folder" — is true and does not help.

What PrestaShop actually does

Credit where it is due. A fresh install ships admin-dev/backups/.htaccess, and it contains exactly what you would want:

# Apache 2.2
<IfModule !mod_authz_core.c>
    Order deny,allow
    Deny from all
</IfModule>

# Apache 2.4
<IfModule mod_authz_core.c>
    Require all denied
</IfModule>
Enter fullscreen mode Exit fullscreen mode

There is an index.php stub next to it, so a directory listing gives you nothing either. The DB Backup page under Advanced Parameters writes into that folder, and PrestaShopBackup builds the name with a random component:

public static $backupDir = '/backups/';
// ...
$rand = dechex(mt_rand(0, min(0xFFFFFFFF, mt_getrandmax())));
$date = time();
$backupfile = $this->getRealBackupPath() . $date . '-' . $rand . '.sql';
Enter fullscreen mode Exit fullscreen mode

So the design is: the folder denies everything, and the filename is not guessable at a glance. Two layers. Fine.

Now the part that matters. The folder is inside the web root_PS_ADMIN_DIR_ is a directory your web server serves. Nothing about that path is outside the document root. The entire protection is one text file that the web server has to choose to read.

Four ways the one text file stops working

1. Your server does not read .htaccess at all.

nginx has no such mechanism. It never had one. An .htaccess file on an nginx host is a text file with no special meaning whatsoever — the deny-all block is inert, and the URL resolves straight to the dump. The same is true of Apache configured with AllowOverride None, which is the recommended production setting in Apache's own documentation for performance reasons.

A large share of managed PrestaShop hosting is nginx, or nginx in front of Apache with static files served by nginx directly, which produces exactly the same outcome for a .sql file. If you have never checked which one you are on, you do not know whether that file is doing anything.

2. The folder can come back without it.

PrestaShopBackup writes the dump. It does not write the .htaccess. That file exists because it shipped with the install, so it survives only as long as nobody removes the folder. Delete the backups folder to clear space and let something recreate it, deploy with a hand-written file list that leaves dotfiles out, restore from an archive built with a glob that skipped them, and the folder comes back with dumps in it and nothing guarding it.

We know this happens because our own cleanup module writes the pair back if they are missing:

if (is_dir($dir) && !file_exists($dir.'.htaccess')) {
    @file_put_contents($dir.'.htaccess', "<Files ~ \"\.(sql|gz)$\">\nOrder Allow,Deny\nDeny from all\n</Files>\n");
}
if (is_dir($dir) && !file_exists($dir.'index.php')) {
    @file_put_contents($dir.'index.php', "<?php\nheader('Location: ../');\nexit;\n");
}
Enter fullscreen mode Exit fullscreen mode

You do not write that check unless you have seen the folder without them.

3. The shop's main .htaccess does not cover it either.

It is tempting to assume PrestaShop's generated root .htaccess — the one the Traffic & SEO page rewrites — has a rule for this. It does not. In 8.2 the only <Files> block it generates protects composer.lock. There is no rule for .sql, no rule for the backups path, no Options -Indexes. So regenerating your .htaccess, which merchants do fairly often, adds nothing here and (if you had hand-edited a rule in) removes it.

4. Most dumps never went through that page.

This is where ours came from, and I suspect where most of them come from. A dump produced by phpMyAdmin's "save on server", by a hosting panel's export button, by mysqldump > backup.sql in whatever directory the SSH session happened to open in — none of those land in the protected folder, and none of them get a random name. They get the name you chose, which is backup.sql, or db.sql, or shop-2026-08.sql.gz, in the directory where you were working, which is the web root.

No amount of correctness in PrestaShopBackup touches this case.

What is in the file

Worth being specific, because "a database dump" sounds abstract and the contents are not.

A PrestaShop dump contains ps_customer and ps_address: names, email addresses, postal addresses, phone numbers, and customer password hashes, for every account the shop has ever had. It contains ps_orders with what everyone bought and ps_order_payment with transaction references. It contains ps_employee, which is your back-office accounts and their password hashes. And it contains ps_configuration, which is where PrestaShop and every module you have installed keep their settings — including the SMTP credentials your shop sends mail with, webservice keys, and whatever API credentials your payment and shipping modules were configured with.

That last one is why "we deleted the file" is not the end of the incident. If a dump was reachable, the credentials in it have to be treated as disclosed, and that means rotating them, not just removing the file.

Checking your own shop, in about two minutes

All of this is on your own server, so there is nothing clever involved:

  1. Find them. From the shop root: find . -maxdepth 3 \( -name '*.sql' -o -name '*.sql.gz' -o -name '*.sql.zip' \). Include your archive extensions. A full-site .zip or .tar.gz from a migration is the same problem wearing a different extension.

  2. Ask your own server for one. Request the exact URL of a file you just found, from outside, with a browser that is not logged in. This is the only test that answers the question, because it is the server's actual configuration answering, not your assumption about it. A 403 is what you want. A 200 with a download is the finding.

  3. Check the backups folder specifically, including whether .htaccess and index.php are still there.

  4. Know which server you are on. nginx -v, or the Server response header, or ask your host. If the answer is nginx, every .htaccess on that host is decoration.

The fix that does not depend on the answer

Move dumps out of the document root. That is the whole fix, and it is the only one that survives a server migration, a control-panel change, or a colleague who does not know the rule. A directory one level above the web root, or an object store, or your laptop. If the file is not addressable by URL, no server configuration can accidentally serve it.

Everything else is mitigation, in descending order of reliability:

  • A server-level rule, in the nginx server block or the Apache vhost, not in an .htaccess. On nginx: a location matching your dump extensions with deny all; and return 404;. In a vhost it cannot be silently disabled by AllowOverride, and it does not vanish when a folder is recreated.
  • Delete the dump when you are done with it. Almost every exposed dump was needed for one afternoon two years ago. A backup you are keeping deliberately belongs in your backup system, which is not a folder in your shop.
  • Rotate what was in it if it was ever reachable. Employee passwords, webservice keys, module API credentials, SMTP.

And if the file is reachable, do not stop at the shop. Search engines index what they can reach, so a dump that sat in the open for a while may be findable independently of your server, which means removing the file is step one of two.

Our own module is not innocent here

Since this is a field note and not marketing: our cleanup module writes its backups into that same folder, and its filenames look like prestacleaner_check-fix-20260827-084500.sql.gz. That is a prefix, a date and a time. It is entirely predictable to anyone who knows the format, which is now everyone reading this.

That is a deliberate trade — the files have to be recognisable in a list so the merchant can tell which is which, and so cleanup never touches a backup somebody else's tool wrote. But it means the naming contributes nothing to secrecy and the folder-level deny is carrying all of it, which is precisely the arrangement this post is about. On an nginx host, that is one layer, and the layer is not there.

The point of writing it down is that we found this in our own shop, by looking. The looking is the part that generalises.


From maintaining around sixty PrestaShop modules at MEG Venture. Verified against PrestaShop 8.2 source: classes/PrestaShopBackup.php, classes/Tools.php, admin-dev/backups/.htaccess.

Top comments (0)