A PHP application should read secrets at runtime from outside the code and the web root, never commit them, never print them, and be able to replace them without a redeploy. Use a secrets store where you can, environment variables where you must, and rotate on a schedule.
Where do PHP secrets usually leak?
PHP has a long history of config living next to code. A database password sits in a config.php inside the document root, a .env file is deployed beside index.php, or a debug page shows everything.
The PHP manual says getenv gets the value of environment variables and that you can see all of them with phpinfo. That is the whole problem in one sentence. A single forgotten phpinfo call on a public route prints every secret the process can see.
Are environment variables enough?
The Twelve-Factor App says config, including credentials to external services, belongs in the environment, not in code. That fixes the commit problem.
The OWASP Secrets Management Cheat Sheet adds a caution: environment variables are generally accessible to all processes and may be included in logs or system dumps, so they are not recommended unless other methods are not possible. It prefers a secrets manager and, for containers, mounted files or short-lived injection.
So the honest position is a ladder. Best is a secrets manager with short leases. Next is a file mounted with tight permissions outside the web root. Environment variables are the fallback, and you then guard against dumps.
What about secrets that are not credentials?
Two PHP built-ins cover the common cases.
- Passwords you store for users: use password_hash and password_verify. The manual describes password_hash as creating a hash with a strong one-way algorithm and handling the salt for you. Never encrypt passwords reversibly.
- Data you must decrypt later: use the libsodium functions. The manual for sodium_crypto_secretbox describes authenticated encryption with a key and a nonce. The nonce must be unique per message, so generate it with random_bytes and store it with the ciphertext.
The encryption key itself is a secret, so it follows the same ladder above.
How do I stop secrets reaching logs and error pages?
Turn off display_errors in production and log errors to a file instead. Redact request bodies that contain credentials. Remove any phpinfo route from every environment that faces the network. Make sure exceptions from your database layer do not include the connection string in the message you show.
Constructed example
A small shop keeps a payment API key in config.php and the file sits under public_html. A server misconfiguration stops PHP from executing for a moment, and the file is served as plain text. The fix has four parts: move the key to a file outside the web root loaded at startup, rotate the exposed key, add the path to the ignore list for version control, and add a CI check that fails on key-shaped strings.
What to do
- Search the repository and its history for keys, passwords and tokens. Treat anything found as exposed and rotate it.
- Move config outside the document root and out of version control.
- Load secrets at runtime from a secrets manager or a restricted file. Use environment variables only as a fallback.
- Hash user passwords with password_hash. Use sodium for reversible encryption with unique nonces.
- Disable display_errors in production and remove phpinfo routes.
- Define a rotation schedule and a way to replace a secret without downtime.
- Add secret scanning to pull requests so a leak is caught before merge.
If you also want to know which of your Composer dependencies carry known issues, Safeguard offers dependency scanning and a free in-browser scan at gold.safeguard.sh/scan for up to 100 dependencies. It does not replace the steps above.
Quick check
Is a .env file safe in production?
Only if it sits outside the web root, is not committed, and has strict file permissions. A secrets manager is a stronger choice per OWASP.
Should I encrypt user passwords?
No. Hash them with password_hash. Encryption is reversible, and you should not need the original password.
Sources
- PHP manual: getenv
- PHP manual: password_hash
- PHP manual: sodium_crypto_secretbox
- OWASP Secrets Management Cheat Sheet
- The Twelve-Factor App: Config
Last reviewed: 18 October 2026.