A freelancer made a quick database export before an update and saved it as backup.sql in the site's public folder, because it was convenient. He meant to delete it afterwards and forgot.
Automated scanners look for exactly this kind of file. Within weeks, the server logs showed repeated requests for common backup filenames, and one of them succeeded. The file contained user records with hashed passwords and email addresses.
The client had to notify its users, reset every password and review its obligations. A single forgotten file turned a routine update into a month of cleanup. The client here was a small membership organisation, and the story is a composite of several incidents with the same ingredients: a convenient location, a predictable name and a delay.
Why the public folder was the convenient place
The freelancer worked over a file manager and a browser-based database tool. His routine before an update was to export the database, then download the export to his laptop. On shared hosting, exports made by the database tool are often written to the account's web directory, and the download link points straight at them. The file manager showed the same folder as the site itself, so putting the file in the top-level public folder meant it was one click away.
That folder, often called public_html or htdocs, is special: everything in it can be fetched by anyone who guesses its name. There is no login in front of a plain file. If the web server is willing to serve a .php file by running it, it is equally willing to serve a .sql file by sending it. Web servers do not know a backup from a stylesheet.
The update went well. The freelancer closed the tab and went to the next job. On his task list the line "remove backup" sat under a heading marked "later".
How scanners found it
Nobody linked to the file. It was not in the sitemap and it did not appear in the site's menus. It did not need to. A large share of the traffic any public server receives is automated: programs that work through lists of common filenames and ask for each one, indefinitely. Among the names on those lists are backup.sql, db.sql, dump.sql, site.zip, www.tar.gz, .env and many variations with years and domain names.
The logs afterwards told the story plainly. Lines like these were scattered through the access log over several weeks:
198.51.100.23 - - [12/Mar/2026:03:14:07 +0000] "GET /backup.sql HTTP/1.1" 404 196
198.51.100.23 - - [12/Mar/2026:03:14:08 +0000] "GET /db.sql HTTP/1.1" 404 196
203.0.113.77 - - [29/Mar/2026:19:42:51 +0000] "GET /backup.sql HTTP/1.1" 200 48211337
The first two are misses, and most of the time the answer is 404. The last is a hit, with a 200 status and a response size of about 46 megabytes. After that single line the data was no longer under the client's control, and there is no way to unsend it.
The timing is typical. The file sat for weeks before the first successful request, which gave a false sense of safety. A scanner may not return for a long time; it may also come back twice in a day.
The window of exposure
The file was reachable from the day it was saved until the day it was removed, and everything in between counts. The success was not the start of the problem; it was only the moment the problem became visible. Anyone reading the logs afterwards has to assume that other copies may have been taken earlier by clients that did not log in any recognisable way, for instance through a caching proxy or a download that the server logged under another address.
This is why the recommended response begins with the clock. The sooner the file is gone, the smaller the window, and the more precise the notice to users can be about what was exposed and when. A vague timeline means a wider notice and more passwords reset than strictly necessary.
What was in the file
A full database export contains everything the application stores. For a membership organisation that meant names, email addresses, postal addresses, membership numbers, the text of messages between members, and a table of users with password hashes.
Hashes are not passwords, but they are not nothing either. A well-chosen modern hash protects strong passwords well. An old, fast hash such as unsalted MD5 protects weak passwords poorly, since a determined attacker can try billions of guesses a second offline. In this case the site used the application's standard scheme, which was better than average, but a share of the members used short or common passwords.
The export also included the application's own settings table. That held the addresses and keys of outside services, such as the mail provider. Those secrets had to be rotated as well.
The response, week by week
The discovery was accidental: the client's webmaster saw the 200 while checking the logs for something unrelated. From then on, the order of work was this.
- Remove the file and confirm that the address now returns 404. This takes a minute and should be done first.
- Establish exactly when the file existed and which requests succeeded, using the access logs. That gave a window and a small number of source addresses.
- Force a password reset for all users, and invalidate existing sessions.
- Rotate every secret that was in the database: mail keys, payment gateway tokens, API tokens.
- Tell the affected people, plainly, what was exposed and what they should do. Because personal data was involved, the organisation also reviewed its legal obligations, including whether the regulator had to be told within the deadline that applies in its country.
- Watch for abuse: phishing aimed at members, and login attempts using the exposed email addresses with passwords from other leaks.
None of this was dramatic, and all of it took time. Replies from members arrived for weeks. A fraction were angry; most were practical. The one thing that helped was the organisation's frankness in the first message.
The fix that lasts
Deleting the file stopped the leak but did not remove the cause. The cause was a habit, plus a server willing to serve any file at all. Three changes dealt with both.
First, backups moved out of the public folder altogether. On most hosting accounts a folder above the public one, in the home directory, is not served. A command-line export to there looks like this:
mysqldump --single-transaction -u dbuser -p dbname | gzip > ~/private/backups/dbname-$(date +%F).sql.gz
Second, the web server was told to refuse downloads of risky extensions even if one is dropped into the public folder by mistake. For Apache, an .htaccess rule does it:
<FilesMatch "\.(sql|bak|old|zip|tar|gz|env|log)$">
Require all denied
</FilesMatch>
On nginx the equivalent is a location ~* \.(sql|bak|zip|gz|env)$ { deny all; } block.
Third, the freelancer's checklist gained a line he could not ignore: a recurring reminder, the same day, to list the public folder and remove anything he had put there.
What would have caught it
- Never store backups inside the web-accessible folder.
- Scan your site's public directory for stray .sql, .zip and .bak files.
- Block downloads of those extensions in the web server configuration.
- Delete temporary files the same day.
- Read the access log for 200 responses on data file extensions every month.