A practical guide to securing your Laravel applications today
Youâve learned the theory. You understand signatures, entropy, behavioral analysis, and why automation matters. Now itâs time to act.
This chapter gives you actionable steps you can take today - right now, with your current tools - to dramatically improve your Laravel applicationâs security posture. No purchases required. No complex setups. Just practical actions that work.
The 15-minute security audit
Donât have 8 hours for a full audit? Start with these 10 checks that catch the most critical issues in 15 minutes.
Check 1: Debug Mode (30 seconds)
# In your project root
grep "APP_DEBUG" .env
# MUST show: APP_DEBUG=false
# If APP_DEBUG=true, you're exposing credentials to attackers Fix immediately if true:
# Edit .env
APP_DEBUG=false
Debug Mode in Production = Game Over
Debug mode exposes your APP_KEY, database credentials, API tokens, and enables XSS attacks through error pages. This is often the first thing attackers check.
Check 2: Environment File Security (1 minute)
# Is .env tracked in git?
git ls-files | grep "^.env$"
# Should return NOTHING
# Is .env in .gitignore?
grep ".env" .gitignore
# Should show .env listed
# Are there backup .env files exposed?
ls -la .env* 2>/dev/null
# Should only show .env, not .env.backup, .env.old, etc. Fix:
# Add to .gitignore if missing
echo ".env" >> .gitignore
echo ".env.backup" >> .gitignore
echo ".env.*.local" >> .gitignore
# Remove any tracked .env files from git history
git rm --cached .env 2>/dev/null
Check 3: APP_KEY Integrity (30 seconds)
# Check APP_KEY format
grep "APP_KEY" .env
# Should start with: APP_KEY=base64:
# Should be 44 characters after base64: // In tinker, verify key length
php artisan tinker
>>> strlen(base64_decode(substr(config('app.key'), 7)))
// Must return: 32
If APP_KEY Is Compromised
If your APP_KEY was ever exposed (in git, logs, errors), attackers can:
- Decrypt all encrypted data
- Forge session cookies
- Execute RCE via deserialization
Generate a new key immediately: php artisan key:generate
Then rotate all encrypted data.
Check 4: Composer Vulnerabilities (2 minutes)
# Check for known vulnerabilities
composer audit
# Should return: No security vulnerability advisories found
# If vulnerabilities found, update immediately If vulnerabilities found:
# Update specific package
composer update vendor/package-name
# Or update all (carefully, test after)
composer update
Priority packages to keep updated:
laravel/frameworklivewire/livewire(CVE-2025-54068!)laravel/sanctumfilament/filament
Check 5: PHP Files in Upload Directories (2 minutes)
# Check storage/app/public
find storage/app/public -name "*.php" -o -name "*.phtml" 2>/dev/null
# Check public/uploads (if exists)
find public/uploads -name "_.php" -o -name "_.phtml" 2>/dev/null
# Check any other upload directories
find public -name "*.php" -not -path "public/index.php" 2>/dev/null
# Should return NOTHING (or only index.php) PHP in Upload Directory = Active Backdoor
If you find ANY .php files in upload directories, assume you are compromised. Do not delete them yet - investigate first, then follow the incident response section at the end of this chapter.
Check 6: Mass Assignment Protection (2 minutes)
# Find models with empty $guarded
grep -rn "guarded = []" app/Models/
# Should return NOTHING
# Empty $guarded allows attackers to set ANY field Fix any found:
// BAD - allows setting any field
protected $guarded = [];
// GOOD - explicitly allow fields
protected $fillable = ['name', 'email', 'password'];
// Or guard sensitive fields
protected $guarded = ['id', 'is_admin', 'role'];
Check 7: Raw SQL Queries (2 minutes)
# Search for potentially vulnerable raw queries
grep -rn "DB::raw|whereRaw|selectRaw|orderByRaw" app/
# Review each result for user input in the raw SQL Vulnerable pattern:
// DANGEROUS - SQL injection possible
DB::select("SELECT * FROM users WHERE id = $id");
$query->whereRaw("status = '$status'");
Safe pattern:
// SAFE - parameterized
DB::select("SELECT * FROM users WHERE id = ?", [$id]);
$query->whereRaw("status = ?", [$status]);
Check 8: Unescaped Blade Output (2 minutes)
# Search for unescaped Blade output
grep -rn "{!!" resources/views/
# Each result is potential XSS - review carefully Review each {!! !!} usage:
// DANGEROUS if $userInput comes from user
{!! $userInput !!}
// SAFE - escaped by default
{{ $userInput }}
// If HTML is needed, sanitize first
{!! clean($trustedHtml) !!}
Check 9: Session Security (1 minute)
// Check config/session.php
return [
'secure' => true, // Must be true (HTTPS only)
'http_only' => true, // Must be true (no JS access)
'same_site' => 'lax', // Should be 'lax' or 'strict'
]; Check 10: Storage Permissions (1 minute)
# Check storage directory permissions
ls -la storage/
# Directories should be 755 (drwxr-xr-x)
# Files should be 644 (-rw-r--r--)
# Fix if needed
chmod -R 755 storage/
chmod -R 755 bootstrap/cache/ Quick detection scripts
Copy these scripts to quickly detect common threats.
Script 1: Webshell Finder
#!/bin/bash
# Quick webshell detection for Laravel apps
echo "=== Laravel Webshell Scanner ==="
echo ""
# Check for common webshell signatures
echo "[1/5] Checking for eval+base64 patterns..."
grep -rln "eval.*base64_decode|base64_decode.*eval" --include="*.php" app/ public/ storage/ 2>/dev/null
echo "[2/5] Checking for system/exec with user input..."
grep -rln "systems*(s*\$_|execs*(s*\$_|passthrus*(s*\$_|shell_execs*(s*\$_" --include="*.php" app/ public/ storage/ 2>/dev/null
echo "[3/5] Checking for assert() usage..."
grep -rln "asserts*(" --include="*.php" app/ 2>/dev/null
echo "[4/5] Checking for create_function()..."
grep -rln "create_functions*(" --include="*.php" app/ 2>/dev/null
echo "[5/5] Checking for preg_replace with /e modifier..."
grep -rln "preg_replace.*/.*e" --include="*.php" app/ 2>/dev/null
echo ""
echo "=== Scan Complete ==="
echo "Review any files listed above carefully." Save as find-webshells.sh and run:
chmod +x find-webshells.sh
./find-webshells.sh
Script 2: Recently Modified PHP Files
#!/bin/bash
# Find PHP files modified in the last 7 days
# Useful for detecting recent infections
echo "=== Recently Modified PHP Files (Last 7 Days) ==="
echo ""
# Exclude vendor and node_modules
find . -name "_.php" -mtime -7 -not -path "./vendor/_" -not -path "./node_modules/_" -not -path "./.git/_" -exec ls -la {} ;
echo ""
echo "Review any unexpected modifications." Script 3: Suspicious File Names
#!/bin/bash
# Find files with suspicious names
echo "=== Suspicious File Detection ==="
echo ""
echo "[1/4] Random-looking PHP filenames..."
find . -name "_.php" -regex "._/[a-z0-9]{6,8}.php" -not -path "./vendor/*" 2>/dev/null
echo "[2/4] Hidden PHP files..."
find . -name "._.php" -not -path "./vendor/_" 2>/dev/null
echo "[3/4] PHP files in unexpected locations..."
find ./public -name "_.php" -not -name "index.php" 2>/dev/null
find ./storage/app -name "_.php" 2>/dev/null
echo "[4/4] Files with double extensions..."
find . -name "_.php._" -o -name "_.jpg.php" -o -name "_.png.php" 2>/dev/null
echo ""
echo "=== Scan Complete ===" Critical configuration hardening
Apply these configurations to all production Laravel applications.
1. Environment Hardening
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com
# Strong session security
SESSION_DRIVER=database
SESSION_SECURE_COOKIE=true
SESSION_LIFETIME=120
# Disable unnecessary features
DEBUGBAR_ENABLED=false
TELESCOPE_ENABLED=false 2. Security Headers Middleware
<?php
namespace AppHttpMiddleware;
use Closure;
use IlluminateHttpRequest;
class SecurityHeaders
{
public function handle(Request $request, Closure $next)
{
$response = $next($request);
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
if ($request->secure()) {
$response->headers->set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
}
return $response;
}
} Register in bootstrap/app.php (Laravel 11) or app/Http/Kernel.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->append(\App\Http\Middleware\SecurityHeaders::class);
})
3. Upload Protection (.htaccess)
# Prevent PHP execution in upload directory
<FilesMatch ".php$">
Order Deny,Allow
Deny from all
</FilesMatch>
# Block common attack files
<FilesMatch "(^.ht|.bak$|.sql$|.zip$)">
Order Deny,Allow
Deny from all
</FilesMatch> Create this file in every directory where users can upload files.
4. Rate Limiting
<?php
use IlluminateSupportFacadesRateLimiter;
use IlluminateCacheRateLimitingLimit;
// In AppServiceProvider or RouteServiceProvider
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->ip());
});
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
// Apply to routes
Route::post('/login', [LoginController::class, 'login'])
->middleware('throttle:login'); Immediate response: If you find something
You ran the scripts and found suspicious files. Now what?
Step 1: Donât Panic, Donât Delete
Do NOT Delete Immediately
Your first instinct will be to delete the malware. Resist it. Deleting destroys evidence you need to: - Understand how they got in - Find other backdoors - Prevent reinfection
Step 2: Document Everything
# Create incident directory
mkdir -p ~/incident-$(date +%Y%m%d)
cd ~/incident-$(date +%Y%m%d)
# Copy suspicious files (don't move)
cp /path/to/suspicious/file.php ./
# Record file metadata
ls -la /path/to/suspicious/file.php > file-metadata.txt
stat /path/to/suspicious/file.php >> file-metadata.txt
# Get file hashes
md5sum /path/to/suspicious/file.php > file-hashes.txt
sha256sum /path/to/suspicious/file.php >> file-hashes.txt
# Check recent access logs
tail -1000 /var/log/apache2/access.log > access-log-sample.txt
# or for nginx
tail -1000 /var/log/nginx/access.log > access-log-sample.txt
Step 3: Isolate (If Active Threat)
If you find an active webshell being used:
# Option 1: Rename to prevent execution (preserves for analysis)
mv suspicious.php suspicious.php.quarantine
# Option 2: Remove execute permissions
chmod 000 suspicious.php
# Option 3: Block at web server (nginx)
# Add to server block:
location ~ /path/to/suspicious\.php {
deny all;
}
Step 4: Find the Entry Point
Check these common entry points:
# 1. Check for recently modified files around infection time
find . -name "*.php" -newer suspicious.php -mtime -1
# 2. Search for upload-related code that might be vulnerable
grep -rn "move_uploaded_file|file_put_contents" app/
# 3. Check web server logs for the suspicious file
grep "suspicious.php" /var/log/*/access.log
# 4. Look for other files created at similar time
ls -la --time-style=full-iso $(dirname suspicious.php)
# 5. Check for known vulnerability patterns
grep -rn "unserialize._\$_" app/
grep -rn "include._\$_|require.*\$_" app/ Step 5: Check for Persistence
Attackers often leave multiple backdoors:
# Check crontabs
crontab -l
cat /etc/crontab
ls -la /etc/cron.*/
# Check for modified Laravel files
# Compare against fresh Laravel install or git
git diff --name-only HEAD
# Check .htaccess files for redirects
find . -name ".htaccess" -exec cat {} ;
# Check for eval in config files
grep -rn "eval|base64_decode" config/
# Check service providers for injected code
grep -rn "eval|system|exec|shell_exec" app/Providers/ Step 6: Clean and Harden
Only after documenting and understanding the breach:
- Remove all identified malware (you documented it first, right?)
- Rotate all credentials:
- APP_KEY (
php artisan key:generate) - Database passwords
- API keys
- User sessions (
php artisan session:clearor truncate sessions table)
- APP_KEY (
- Update all dependencies:
composer update - Apply all security hardening from this chapter
- Monitor closely for 2-4 weeks for reinfection
Weekly security routine
Once youâve done the initial hardening, maintain security with this weekly routine:
| Day | Task | Time |
|---|---|---|
| Monday | Run composer audit | 2 min |
| Wednesday | Run webshell detection script | 5 min |
| Friday | Check recent file modifications | 5 min |
| Weekly | Review error logs for anomalies | 10 min |
Total time: ~22 minutes per week
This wonât catch everything, but itâs infinitely better than nothing - and itâs sustainable.
The gap that remains
You now have practical steps to improve security immediately. But letâs be honest about the limitations:
| What You CAN Do Manually | What You CANâT Do Manually |
|---|---|
| Weekly spot checks | Hourly monitoring |
| Known signature detection | AI polymorphism detection |
| Basic configuration audit | Behavioral analysis |
| Reactive response | Proactive prevention |
| Single-site focus | Multi-site coordination |
Manual Security Has Limits
The steps in this chapter dramatically improve your security posture. But they donât solve the fundamental problem from Chapter 8: you canât be everywhere at once, and threats donât wait for your schedule.
The scripts and checks in this chapter are necessary but not sufficient. Theyâre the security equivalent of locking your front door - essential, but not a complete security system.
For comprehensive protection, you need:
- Continuous monitoring (not weekly checks)
- Automated response (not morning-after discovery)
- Intelligent detection (not just pattern matching)
- Behavioral analysis (not just signatures)
Thatâs where automated scanning comes in.
Summary
Immediate actions (do today):
- Verify
APP_DEBUG=false - Run
composer auditand update vulnerable packages - Check for PHP files in upload directories
- Review
.envsecurity - Add upload directory
.htaccessprotection
Quick detection scripts:
find-webshells.sh- Basic signature detectionrecent-php-changes.sh- Modified file detectionsuspicious-files.sh- Anomaly detection
If you find malware:
- Document first, delete later
- Find the entry point
- Check for persistence
- Clean, rotate credentials, harden
- Monitor for reinfection
Weekly routine:
- 22 minutes of focused security checks
- Better than nothing, but has limits
Youâve taken the first steps. Youâve hardened your application. Youâve set up basic monitoring. Thatâs more than most Laravel developers ever do.
But you know the threats are evolving faster than manual checks can keep up. You know AI-generated malware changes every 15 seconds. You know that weekly audits leave 6-day exposure windows.
The next chapter introduces a solution that addresses these gaps.
Next: Chapter 11 - Meet Your New Security Partner
Youâve done what you can manually. Now meet the tool that does the rest - 24/7, automatically, while you focus on building features.