Cyberattacks hit every public site especially small apps within minutes of going live. This article shares the practical, copy-pasteable hardening routine I use to keep a Laravel/PHP site safe on shared hosting: lock down secrets and permissions, tighten auth and input handling, secure uploads, add security headers, rate-limit behind a WAF, and set up monitoring and backups. No theory just a baseline you can implement today and improve over time.
.env essentials:
APP_ENV=production
APP_DEBUG=false
APP_URL=https://example.com
LOG_CHANNEL=stack
LOG_LEVEL=warning
SESSION_SECURE_COOKIE=true
SESSION_HTTP_ONLY=true
SESSION_SAME_SITE=lax
RewriteEngine On RewriteCond %{HTTPS} !=on RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
Keep .env out of public_html (place it in /home/USER/laravel/.env). Permissions (typical baseline):
# from project root (NOT public_html) find . -type f -exec chmod 0644 {} \; find . -type d -exec chmod 0755 {} \; chmod -R 0775 storage bootstrap/cache chmod 0640 .env
Password policy (in App\Providers\AuthServiceProvider or a dedicated provider):
use Illuminate\Validation\Rules\Password; Password::defaults(function () { return Password::min(12)->mixedCase()->numbers()->symbols()->uncompromised(); });
Throttle logins (in App\Providers\RouteServiceProvider
):
use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Support\Facades\RateLimiter; use Illuminate\Http\Request; public function boot(): void { RateLimiter::for('login', function (Request $request) { $email = (string) $request->input('email'); return Limit::perMinute(5)->by($email.$request->ip()); }); }
Apply middleware to login route: ->middleware('throttle:login').
2FA: If you use Jetstream/Fortify, enable TOTP; otherwise, add a lightweight package or an SMS/Email code for admin logins.
Session hardening: already set via .env. Also set a short session lifetime for admin areas.
Always validate requests:
$request->validate([ 'title' => ['required','string','max:120'], 'email' => ['required','email'], 'page' => ['nullable','integer','min:1','max:1000'], ]);
Use {{ $title }} (escaped) rather than {!! $title !!} (only use unescaped when you fully trust/sanitize the content).
Avoid raw SQL: Eloquent and the query builder use prepared statements by default. If you must use DB::raw(), sanitize carefully and prefer bindings.
CSRF: All forms need @csrf. APIs should be in routes/api.php (stateless, no CSRF).