Secrets in Laravel โ€” Why `.env` Is Only the Beginning
DEV Community

Secrets in Laravel - Why .env Is Only the Beginning

Originally published on Medium Most developers think adding .env to .gitignore solves secret management. It doesn't. This is the eleventh article in a series on PHP and Laravel application security. So far we have covered: - Detecting SQL injection attempts in PHP logs - Why URL encoding blinds most PHP security checks - The decode bomb problem with unlimited URL decoding - Why parameterized queries are the only real fix for SQL injection - XSS prevention in Laravel and why {!! !!} is the line between safe and hacked - How attackers enumerate your Laravel app before exploiting it - File upload security - the file that isn't what it claims to be - Path traversal in PHP - how ../ escapes your application - Command injection in PHP - when exec() becomes an attack surface - Broken access control in Laravel - why being logged in is not enough Every article in this series follows the same principle: understand the attack before you try to stop it. Secrets are different from every other topic in this series. SQL injection, XSS, path traversal those are vulnerabilities in your code. Secret leakage is a vulnerability in your process. It happens not because you wrote something wrong but because you stored, logged, or committed something in the wrong place. And unlike a code vulnerability that can be patched an exposed secret that has been harvested by an automated scanner cannot be un-exposed. The only fix is rotation. What Secrets Actually Are Not all configuration is equal. There is a meaningful difference between configuration and secrets. Configuration controls how your application behaves: APP_NAME=MyApp APP_URL=https://myapp.com MAIL_MAILER=smtp DB_HOST=localhost Secrets grant access to systems, services, or data: DB_PASSWORD=supersecretpassword STRIPE_SECRET_KEY=sk_live_abc123 AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI MAIL_PASSWORD=mypassword APP_KEY=base64:abc123... Configuration can often be committed to version control. Secrets should never be. The distinction matters because secrets require fundamentally different handling different storage, different access controls, and a rotation plan for when they are compromised. Most Laravel developers put both in .env and add .env to .gitignore . That solves one problem. It leaves five others untouched. How Secrets Actually Leak During internal testing of Kriosa, automated scanners were repeatedly observed probing for exposed .env files, backup archives, and forgotten configuration endpoints. Those reconnaissance requests happen long before an attacker attempts exploitation which is why secret exposure should be treated as both a prevention and a detection problem, not just a deployment concern. Here is every vector through which secrets leave applications in practice. Leak Vector 1 - Git History A developer accidentally commits .env : git add . git commit -m "initial commit" They realize the mistake and delete the file: git rm .env git commit -m "remove env file" They think the problem is solved. It is not. Git is designed to never lose history. The .env file and every secret it contained are permanently stored in the Git object database. Anyone with access to the repository can retrieve them: git log --all --full-history -- .env git show [commit-hash]:.env Every secret that was ever in that file is recoverable even after deletion - unless the entire Git history is rewritten with a tool like git filter-repo . Automated tools scan public GitHub repositories continuously looking for accidentally committed secrets. Within minutes of a secret being pushed to a public repository it has likely been harvested. What to do: - Add .env to.gitignore before the first commit not after - If secrets were committed use git filter-repo to rewrite history - Immediately rotate every secret that was ever in the committed file - Never assume deletion from Git removes the data Leak Vector 2 - Log Files Laravel logs requests, errors, and debug information. If your application logs user input or request data and many do secrets can end up in log files in plain text. Common patterns that cause secret leakage into logs: // Logs everything - including Authorization headers and API keys Log::info('Incoming request', $request->all()); // Logs the full exception context - may include secrets in memory Log::error('Payment failed', ['data' => $request->all()]); // Logs API responses that may contain tokens Log::debug('API response', ['response' => $apiResponse]); If a user submits a form with a field named api_key and you log $request->all() that key is now in your log file in plain text. Laravel provides a way to prevent specific fields from appearing in logs through the exception handler: // app/Exceptions/Handler.php - Laravel 10 and earlier protected $dontFlash = [ 'current_password', 'password', 'password_confirmation', ]; Laravel does not automatically hide custom fields like api_key , token , or stripe_key . If your application accepts them extend the $dontFlash array or explicitly avoid logging those values: protected $dontFlash = [ 'current_password', 'password', 'password_confirmation', 'api_key', 'token', 'secret', 'stripe_key', 'aws_key', ]; Add every secret field name your application uses to this list. Leak Vector 3 - Debug Mode and Error Pages When APP_DEBUG=true in production, Laravel's error pages can expose sensitive configuration values, stack traces, file paths, and application internals. Depending on the error and configuration, this may include secrets or values derived from your environment. APP_DEBUG=false โ† non-negotiable in production APP_ENV=production Even with debug mode off, verbose error handling can expose secrets: // Stack traces may contain secret values that were in memory at the point of failure Log::error($exception->getMessage(), $exception->getTrace()); Log the message. Be careful with the full trace when secrets may be in scope at the point of failure. Leak Vector 4 - Hardcoded Secrets in Code Developers sometimes hardcode secrets directly in code during development and forget to move them to environment variables before committing: // Dangerous - committed to version control, visible to every developer $stripe = new StripeClient('sk_live_abc123def456'); // Safe - loaded from config $stripe = new StripeClient(config('services.stripe.secret')); Hardcoded secrets are committed to version control, stored in deployment artifacts, visible in code reviews, and accessible to every developer with repository access. They are also the hardest to rotate because you have to find every place the secret appears in the codebase. Leak Vector 5 - Third-Party Logging Services Many Laravel applications send logs to third-party services Papertrail, Loggly, Datadog, Sentry. If secrets appear in your application logs they appear in these services too with their own retention policies, access controls, and security posture that you do not fully control. If you send logs to a third-party service audit what is in those logs. Secrets that appear there need to be rotated and the logging configuration needs to be fixed before the rotation has any meaningful effect. Laravel-Specific Secret Management The APP_KEY Laravel's APP_KEY is the most sensitive secret in a Laravel application. It is used to encrypt cookies, session data, and model fields encrypted with Laravel's encryption. If your APP_KEY is compromised an attacker can decrypt all encrypted session data, forge signed cookies, and decrypt any fields encrypted with Laravel's encryption. Rotate it immediately if it is ever exposed but be aware that rotating the APP_KEY invalidates all existing sessions and encrypted data. # Generate a new APP_KEY php artisan key:generate Using config() instead of env() directly This is a Laravel best practice that most developers miss: // Wrong - does not work correctly when config is cached $key = env('STRIPE_SECRET_KEY'); // Correct - works with config caching $key = config('services.stripe.secret'); Once configuration is cached with php artisan config:cache , you should only call env() from configuration files. Application code should access configuration through config() , ensuring values continue to work correctly when configuration is cached. Define secrets in config files: // config/services.php 'stripe' => [ 'secret' => env('STRIPE_SECRET_KEY'), 'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'), ], Then access them through config() in your application code: $key = config('services.stripe.secret'); Laravel's encrypted .env Recent versions of Laravel support encrypted environment files: # Encrypt your .env file safe to commit the encrypted version php artisan env:encrypt # Decrypt on the server using the encryption key php artisan env:decrypt --key=[encryption-key] This allows you to commit an encrypted .env file to version control while keeping the decryption key separate. The decryption key itself must be stored and transmitted securely if it is compromised the encrypted file offers no protection. Secret Rotation Secret rotation means replacing a compromised or expired secret with a new one. Most developers never think about rotation until something goes wrong. By then the window of exposure may already be significant. Rotate immediately when: - A secret was committed to a public repository - A developer with access to secrets leaves the team - A third-party service you use reports a breach - Your server was compromised - A secret appeared in logs that were accessed without authorization - You suspect any unauthorized access to a system that holds secrets Laravel rotation checklist: - Generate a new value for the compromised secret - Update .env on every server - Update the secret in CI/CD pipelines and deployment configurations - Revoke the old secret at the source revoke the API key, change the database password, regenerate the APP_KEY - Verify the application works with the new secret - Audit logs for evidence of use of the compromised secret The rotation problem most developers ignore: You cann

Comments

No comments yet. Start the discussion.