🔒 Hacked
Chapter 10

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.

15 min
Quick Audit
5
Critical Fixes
3
Detection Scripts
Today
Start Now

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)

Check Debug Mode
# 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)

Check .env Security
# 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
# 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)

Run Composer Audit
# 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:


Check 5: PHP Files in Upload Directories (2 minutes)

Find PHP in Uploads
# 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)

Check for Unguarded Models
# 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)

Find Raw Queries
# 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)

Find Unescaped Output
# 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 Session Config
// 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 Permissions
# 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

find-webshells.sh
#!/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

recent-php-changes.sh
#!/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

suspicious-files.sh
#!/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

.env (Production)
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

app/Http/Middleware/SecurityHeaders.php
<?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)

storage/app/public/.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

routes/web.php
<?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:

Entry Point Investigation
# 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:

Persistence Check
# 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:

  1. Remove all identified malware (you documented it first, right?)
  2. Rotate all credentials:
    • APP_KEY (php artisan key:generate)
    • Database passwords
    • API keys
    • User sessions (php artisan session:clear or truncate sessions table)
  3. Update all dependencies: composer update
  4. Apply all security hardening from this chapter
  5. Monitor closely for 2-4 weeks for reinfection

Weekly security routine

Once you’ve done the initial hardening, maintain security with this weekly routine:

DayTaskTime
MondayRun composer audit2 min
WednesdayRun webshell detection script5 min
FridayCheck recent file modifications5 min
WeeklyReview error logs for anomalies10 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 ManuallyWhat You CAN’T Do Manually
Weekly spot checksHourly monitoring
Known signature detectionAI polymorphism detection
Basic configuration auditBehavioral analysis
Reactive responseProactive prevention
Single-site focusMulti-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:

That’s where automated scanning comes in.


Summary

Immediate actions (do today):

  1. Verify APP_DEBUG=false
  2. Run composer audit and update vulnerable packages
  3. Check for PHP files in upload directories
  4. Review .env security
  5. Add upload directory .htaccess protection

Quick detection scripts:

If you find malware:

  1. Document first, delete later
  2. Find the entry point
  3. Check for persistence
  4. Clean, rotate credentials, harden
  5. Monitor for reinfection

Weekly routine:

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.