Many breaches are not clever attacks. A password sits in a file that anyone can download, or a backup is left on the server, or an error page prints the database name. This lesson is about not leaving the door open.
Secrets: where they live
Database passwords, API keys and mail credentials must not be:
- Written in code that is committed to Git.
- Stored in a file inside the public web folder.
- Printed in error messages or logs.
- Sent to the browser (in JavaScript, HTML comments or JSON).
They belong in environment variables or a config file outside the web root. See Environment Variables.
<?php
putenv("API_KEY=sk_live_example");
$key = getenv("API_KEY");
echo $key !== false ? "The key is read from the environment" : "missing";
The key is read from the environment
Files that must never be public
| File | Risk |
|---|---|
.env, config.php copies (config.php.bak, config.old) | Passwords and keys |
.git/ folder | The whole source code history, with old secrets |
Database dumps (backup.sql, site.zip) | All your data |
composer.json, composer.lock, vendor/ | Reveal exact library versions to attackers |
phpinfo() pages and test scripts | Show your configuration |
| Log files | Often contain personal data and tokens |
Editor and OS files (.DS_Store, .idea/) | Reveal the folder structure |
Keep application code outside the public folder. Only public/index.php and static assets belong in it.
On Apache, you can deny dotfiles:
<FilesMatch "^\.">
Require all denied
</FilesMatch>
Options -Indexes
Options -Indexes turns off directory listings.
Search for leaks
From your project folder, check what Git tracks:
git ls-files | grep -i -E "\.env|\.sql|\.bak|backup|id_rsa"
If a secret was ever committed, changing the file is not enough. The old version stays in Git history. Treat that secret as compromised: change it (rotate it) at the source.
Do not show errors
Turn off display_errors on live sites, and show a friendly page. See Production Error Handling.
Return only what is needed
An API or a page should include only the fields that the viewer needs.
<?php
$user = ["id" => 7, "name" => "Ana", "email" => "ana@example.com", "password_hash" => '$2y$10$abc', "role" => "admin"];
$public = array_intersect_key($user, array_flip(["id", "name"]));
echo json_encode($public);
{"id":7,"name":"Ana"}
Select only needed columns in SQL (not SELECT *), and build the output from an allow-list of fields. Then a new sensitive column never leaks by accident.
Encrypt sensitive data you must store
For things like API tokens or personal data that you must read back, use authenticated encryption (sodium_crypto_secretbox, or a library). Keep the key outside the database.
<?php
$key = sodium_crypto_secretbox_keygen();
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$cipher = sodium_crypto_secretbox("secret token", $nonce, $key);
echo sodium_crypto_secretbox_open($cipher, $nonce, $key);
secret token
This needs the sodium extension, which ships with PHP but must be enabled (extension=sodium). Passwords are not encrypted. They are hashed. Never store card numbers yourself. Use a payment provider that handles them.
Collect less
Data you never collect cannot leak. Ask: do we need the date of birth? The full address? Delete data when you no longer need it, and follow the privacy rules that apply to your visitors.
Backups
Keep backups, store them off the web server, encrypt them, and test restoring them. A backup in /public_html/backup.zip is a gift to attackers.
HTTPS
Without HTTPS, every login and form can be read by anyone on the network. Use it everywhere.