The best measure to do this is to store such file on a directory outside the web root.
Basically, all of the solutions 'solve' the problem, in making the password file not available. Their differences stem on the kind of configuration error that needs to happen for it to break.
Surprisingly, such misconfigurations do happen from time to time, (e.g. when updating or reinstalling a system) and thee solutions simply vary on how robust they are to them. Also, sometimes these configurations are set in .htaccess files, but then a config changed at the server and they were not applied.
- Breaks if the web server generated an Index list.
- Fails if the piece configuring the web server not to serve .secret files was not added/applied, or if an editor left a backup file with an unprotected extension
- Would fail if the PHP files were not being interpreted (mod_php not loaded?) or if the password was not changed from the default.
- Would also fail if the PHP files were not being interpreted
Placing the file outside the web root (eg.
/etc/php-app/hashed_passwords.txt) makes it harder that it gets downloaded by a malicious actor.
Obviously, nothing stops you from combining several of these methods (although at some point that would win you little).
Note: if using the .php route, you shall make sure that a malicious user cannot insert code into the generated files, leading to a code execution vulnerability. Generally this means you will die at the beginning (e.g. starting the file with
<?php die(1);). You may also use __halt_compiler to ensure that no part of the rest of the file is interpreted by PHP.
The main difference I see on using files vs connecting to a database server (note you would face the same dilemma of how to name the file if you used SQlite) would be on its robustness for race conditions. Suppose that a user is performing a change password action at the same time that another one is signing up. A naive implementation using files would be vulnerable to a race condition where the recently created user would be lost when replacing the file with the new password hash, whereas a database would automatically care take of that (a naive implementation would be vulnerable to SQL injection, though).
Other differences would be at permissions, backups and scalability:
- If the app connects to a database, the code itself could be read-only at the filesystem, whereas the files with information stored need write access there (and if you are using .php files, they need to be able to write into a file where code could be executed if written into, so a file-writing vulnerability would automatically become an execution vulnerability).
- If there is already a system in place to backup the database server, that is easiest, rather than having to do a custom backup of a weirdly named file. On the other hand, using a file means that simply copying the directory results in a full backup, no need to dump the db separately.
- If you may end up scaling horizontally, it is better to go with a normal database. Often simply making several nodes point to the same database make them all work (you should share the sessions, too). If you used a file database, that means you would have to share that file (by nfs, perhaps?) and have it writable by the multiple nodes (with all the added complication of race conditions, just with more nodes and a less capable filesystem layer).
And obviously, running a database server imposes certain overhead. If there isn't already a need to be using one, and the server resources are constrained, it may be preferable to use files simply to be lighter.
As you see, mostly the reasons that would lead your decision of using a file vs a database server are not about security, since you could have an equivalently secure app in both cases.