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).