Laravel Sicherheitsrichtlinien
Umfassende Sicherheitsrichtlinien für Laravel-Anwendungen zum Schutz vor häufigen Schwachstellen.
Wann aktivieren
- Einrichten der Laravel-Authentifizierung und -Autorisierung (Sanctum, Passport, Jetstream, Breeze)
- Implementieren von Benutzerrollen, -berechtigungen und -richtlinien
- Konfigurieren von Produktionssicherheitseinstellungen und Umgebungsvariablen
- Überprüfen von Laravel-Anwendungen auf Sicherheitslücken
- Bereitstellen von Laravel-Anwendungen in der Produktion
- Schreiben sicherer Eloquent-Abfragen und Migrationen
Produktionskonfiguration
Wesentliche Produktionseinstellungen
// config/app.php
'env' => env('APP_ENV', 'production'),
'debug' => (bool) env('APP_DEBUG', false), // KRITISCH: Niemals true in Produktion
'key' => env('APP_KEY'), // Muss gesetzt sein: php artisan key:generate
// config/session.php
'secure' => env('SESSION_SECURE_COOKIE', true),
'http_only' => true,
'same_site' => 'lax',
// Überprüfen, ob APP_KEY beim Booten gesetzt ist
// bootstrap/app.php oder ein Service Provider
if (empty(config('app.key'))) {
throw new RuntimeException('APP_KEY ist nicht gesetzt. Führen Sie aus: php artisan key:generate');
}
Umgebungsdatei-Sicherheit
# NIE .env in die Versionskontrolle einchecken
# .gitignore enthält standardmäßig bereits .env
# Stattdessen .env.example mit Platzhaltern verwenden
DB_PASSWORD=
APP_KEY=
SANCTUM_TOKEN_PREFIX=
# Erforderliche Variablen beim Booten validieren
// In AppServiceProvider::boot()
$requiredKeys = ['app.key', 'database.connections.mysql.database', 'database.connections.mysql.username'];
foreach ($requiredKeys as $key) {
if (empty(config($key))) {
throw new RuntimeException("Fehlender erforderlicher Konfigurationsschlüssel: {$key}");
}
}
HTTPS-Erzwingung
// AppServiceProvider::boot() oder Middleware
if (app()->environment('production')) {
URL::forceScheme('https');
request()->server->set('HTTPS', 'on');
}
// config/app.php für vertrauenswürdige Proxys (Load Balancer)
// Verwenden Sie spezifische IP-Bereiche — * vertraut allen, was X-Forwarded-*-Spoofing ermöglicht
// AWS: '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'
'trusted_proxies' => ['10.0.0.0/8', '172.16.0.0/12'],
// HTTPS in Produktion via Middleware erzwingen
// app/Http/Middleware/ForceHttps.php
public function handle($request, Closure $next)
{
if (!$request->secure() && app()->environment('production')) {
return redirect()->secure($request->getRequestUri());
}
return $next($request);
}
Authentifizierung
Sanctum (API-Token-Authentifizierung)
// config/sanctum.php
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
env('APP_URL') ? ',' . parse_url(env('APP_URL'), PHP_URL_HOST) : ''
)));
'expiration' => 60 * 24, // Token-Ablauf in Minuten (null = nie)
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
// Ausstellen von Token mit Fähigkeiten
$token = $user->createToken('api-token', ['read', 'write'])->plainTextToken;
// Fähigkeiten auf Routen validieren
Route::middleware('auth:sanctum')->group(function () {
Route::get('/orders', function () {
// Benutzer muss 'read'-Fähigkeit haben
abort_unless(Auth::user()->tokenCan('read'), 403);
// ...
})->middleware('abilities:read');
Route::post('/orders', function () {
// Benutzer muss 'write'-Fähigkeit haben
abort_unless(Auth::user()->tokenCan('write'), 403);
// ...
})->middleware('abilities:write');
});
Passwortsicherheit
// config/hashing.php
// Standard ist bcrypt. Argon2id ist stärker.
'bcrypt' => [
'rounds' => env('BCRYPT_ROUNDS', 12), // Erhöhen für stärkeres Hashing
],
'argon' => [
'memory' => 65536,
'threads' => 4,
'time' => 4,
],
// Passwortvalidierung in RegisterRequest
public function rules(): array
{
return [
'password' => [
'required',
'confirmed',
Password::min(12)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised(), // Prüft haveibeenpwned
],
];
}
// Login-Versuche ratelimitieren
// App\Http\Controllers\Auth\AuthenticatedSessionController
protected function authenticated(Request $request, $user)
{
if ($user->wasRecentlyLockedOut()) {
// Benutzer über verdächtigen Login benachrichtigen
$user->notify(new SuspiciousLoginNotification($request->ip()));
}
}
Sitzungsverwaltung
// config/session.php
'driver' => env('SESSION_DRIVER', 'database'), // database/redis > file
'lifetime' => env('SESSION_LIFETIME', 120),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
'encrypt' => env('SESSION_ENCRYPT', false),
// Sitzung bei Login regenerieren
// App\Http\Controllers\Auth\AuthenticatedSessionController
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate(); // KRITISCH: verhindert Session Fixation
return redirect()->intended(RouteServiceProvider::HOME);
}
// Sitzung bei Logout ungültig machen
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
Autorisierung
Gates
// App\Providers\AuthServiceProvider
use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
public function boot(): void
{
Gate::define('update-post', function (User $user, Post $post): bool {
return $user->id === $post->user_id;
});
Gate::define('publish-post', function (User $user): bool {
return $user->role === 'editor' || $user->role === 'admin';
});
// Verwendung von before() für Super-Admin-Überschreibung
Gate::before(function (User $user, string $ability): ?bool {
if ($user->role === 'super-admin') {
return true; // Gewährt alle Fähigkeiten
}
return null; // Fallback auf normale Prüfungen
});
}
// Verwendung in Controllern
public function update(Request $request, Post $post): RedirectResponse
{
Gate::authorize('update-post', $post);
// Oder: $this->authorize('update-post', $post);
// Oder: abort_unless(Auth::user()->can('update-post', $post), 403);
// ...
}
Policies
// App\Policies\PostPolicy
class PostPolicy
{
use HandlesAuthorization;
public function viewAny(?User $user): bool
{
return true; // Öffentliche Auflistung
}
public function view(?User $user, Post $post): bool
{
return $post->is_published || ($user && $user->id === $post->user_id);
}
public function create(User $user): bool
{
return $user->hasVerifiedEmail(); // Muss E-Mail zuerst bestätigen
}
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
public function delete(User $user, Post $post): bool
{
return $user->id === $post->user_id && $post->created_at->diffInDays(now()) <= 30;
}
public function restore(User $user, Post $post): bool
{
return $user->role === 'admin';
}
public function forceDelete(User $user, Post $post): bool
{
return $user->role === 'super-admin';
}
}
// Registrieren in AuthServiceProvider
protected $policies = [
Post::class => PostPolicy::class,
];
// Controller-Verwendung
public function show(Post $post): View
{
$this->authorize('view', $post);
return view('posts.show', compact('post'));
}
// Blade-Verwendung
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">Bearbeiten</a>
@endcan
@cannot('update', $post)
<span>Sie können diesen Beitrag nicht bearbeiten</span>
@endcannot
Middleware-Autorisierung
// Verwendung von Middleware in Routen
Route::put('/posts/{post}', [PostController::class, 'update'])
->middleware('can:update,post');
Route::get('/posts/create', [PostController::class, 'create'])
->middleware('can:create,App\Models\Post');
// Benutzerdefinierte Autorisierungs-Middleware
// app/Http/Middleware/CheckRole.php
class CheckRole
{
public function handle(Request $request, Closure $next, string $role): mixed
{
if (!$request->user() || $request->user()->role !== $role) {
abort(403, 'Unautorisiert. Dieser Bereich erfordert Rolle: ' . $role);
}
return $next($request);
}
}
// Registrieren in Kernel
protected $routeMiddleware = [
'role' => \App\Http\Middleware\CheckRole::class,
];
// Routenverwendung
Route::middleware(['auth', 'role:admin'])->group(function () {
Route::get('/admin', [AdminController::class, 'index']);
});
Eloquent-Sicherheit
Massenzuweisungsschutz
// SCHLECHT: $guarded = [] erlaubt Massenzuweisung ALLER Spalten
// NIE $guarded = [] in Produktion verwenden
// GUT: Whitelist fillable-Attribute
final class User extends Authenticatable
{
protected $fillable = [
'name',
'email',
'phone',
'avatar',
];
// NIE 'role', 'is_admin', 'is_verified' hier hinzufügen
}
// GUT: Explizit kontrollieren, welche Felder in Requests befüllt werden können
public function store(StoreUserRequest $request): RedirectResponse
{
$user = User::create($request->safe()->only([
'name', 'email', 'phone', 'avatar'
]));
// $request->safe() verwendet nur validierte Daten
// $request->only() ist ohne Validierungsregeln NICHT sicher
}
// SCHLECHT: Benutzer mit Request-Daten direkt erstellen
User::create($request->all()); // ANGREIFBAR für Massenzuweisung!
// BESSER: DTOs für die Erstellung verwenden
$user = User::create($request->validated()); // Nur validierte Felder
SQL-Injection-Prävention
// GUT: Eloquent parametrisiert Abfragen automatisch
User::where('email', $userInput)->first();
User::whereRaw('email = ?', [$userInput])->first();
// GUT: Query Builder parametrisiert ebenfalls
DB::table('users')->where('email', $userInput)->first();
DB::select('SELECT * FROM users WHERE email = ?', [$userInput]);
// SCHLECHT: Rohe String-Interpolation
DB::select("SELECT * FROM users WHERE email = '{$userInput}'"); // ANGREIFBAR!
User::whereRaw("email = '{$userInput}'")->first(); // ANGREIFBAR!
// SCHLECHT: whereRaw/orderByRaw mit unescaped Eingabe
User::orderByRaw($userInput); // ANGREIFBAR!
User::groupByRaw($userInput); // ANGREIFBAR!
// SCHLECHT: DB::statement mit Verkettung
DB::statement("INSERT INTO users (email) VALUES ('{$userInput}')"); // ANGREIFBAR!
Attribut-Casting
final class User extends Authenticatable
{
protected $casts = [
'email_verified_at' => 'datetime',
'is_admin' => 'boolean', // Cast zu Boolean verhindert String-Injection
'settings' => 'array', // Automatisch json_encode/json_decode
'metadata' => 'encrypted:array', // Laravel 11+ verschlüsseltes Casting
'password' => 'hashed', // Laravel 10+ automatisches Hashing beim Setzen
];
}
Modellsicherheit
final class User extends Authenticatable
{
// Sensitive Attribute vor JSON/API-Antworten verbergen
protected $hidden = [
'password',
'remember_token',
'two_factor_secret',
'two_factor_recovery_codes',
];
// Nur sichere berechnete Attribute anhängen
protected $appends = ['full_name']; // sicher
// NIE sensitive berechnete Daten anhängen
}
final class Post extends Model
{
// Global Scope zum Filtern von Soft-Deleted-Datensätzen
use SoftDeletes;
// N+1 verhindern durch Einschränken von Lazy Loading (optionaler Strict-Modus)
// AppServiceProvider::boot()
// Model::preventLazyLoading(!app()->isProduction());
}
CSRF-Schutz
Standard-Schutz
// Laravel CSRF ist standardmäßig via VerifyCsrfToken-Middleware aktiviert
// app/Http/Kernel.php (protected $middlewareGroups['web'])
// Alle POST/PUT/PATCH/DELETE-Formulare müssen @csrf enthalten
<form method="POST" action="/posts">
@csrf
<input type="text" name="title">
<button type="submit">Erstellen</button>
</form>
Routen ausschließen (Vorsichtig)
// app/Http/Middleware/VerifyCsrfToken.php
class VerifyCsrfToken extends Middleware
{
// Nur Routen ausschließen, die externen CSRF-Schutz haben (Webhooks, etc.)
protected $except = [
'stripe/*', // Stripe-Webhooks verwenden eigene Signaturprüfung
// Vermeiden Sie pauschale 'api/*' — stateful Sanctum-Routen benötigen CSRF.
// Schließen Sie nur spezifische zustandslose Webhook-/Endpunkt-Routen aus.
];
}
CSRF mit JavaScript
<meta name="csrf-token" content="{{ csrf_token() }}">
<script>
// Axios-Beispiel (Laravel wird mit Axios ausgeliefert)
axios.defaults.headers.common['X-CSRF-TOKEN'] = document.querySelector(
'meta[name="csrf-token"]'
).getAttribute('content');
// Fetch-Beispiel
fetch('/posts', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
</script>
XSS-Prävention
Blade-Templating-Sicherheit
{{-- SICHER: Automatisch von Blade escaped --}}
{{ $userInput }}
{{-- GEFÄHRLICH: Rohe Ausgabe — NIE mit Benutzereingaben verwenden --}}
{!! $userInput !!}
{{-- SICHER: {!! !!} nur mit vertrauenswürdigem, selbst kontrolliertem Inhalt verwenden --}}
{!! $trustedHtmlFromYourServer !!}
{{-- GUT: Spezifische Escaping-Direktiven verwenden --}}
@js($data) {{-- JSON-Encode für JavaScript --}}
@json($data) {{-- JSON-Encode in Templates --}}
{{-- SCHLECHT: Direkte Benutzereingaben in rohem HTML --}}
<div>{!! $user->bio !!}</div> {{-- ANGREIFBAR, wenn Benutzer Bio bereitstellt --}}
Sicheres HTML-Handling
// Wenn etwas HTML erlaubt werden muss, Whitelist-Ansatz verwenden
use HTMLPurifier; // Erfordert: composer require ezyang/htmlpurifier
public function sanitizeHtml(string $dirty): string
{
$config = \HTMLPurifier_Config::createDefault();
$config->set('HTML.Allowed', 'p,b,i,a[href],ul,ol,li,br');
$config->set('URI.AllowedSchemes', ['http', 'https', 'mailto']);
$purifier = new \HTMLPurifier($config);
return $purifier->purify($dirty);
}
// In Blade:
<div>{!! $sanitizedHtml !!}</div> {{-- SICHER nach Bereinigung --}}
Content Security Policy (CSP)
// CSP-Header via Middleware setzen
// app/Http/Middleware/ContentSecurityPolicy.php
class ContentSecurityPolicy
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->header('Content-Security-Policy', "
default-src 'self';
script-src 'self' 'nonce-" . base64_encode(random_bytes(16)) . "' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
form-action 'self';
frame-ancestors 'none';
base-uri 'self';
object-src 'none';
");
return $response;
}
}
// CSP-Nonce in Blade verwenden
<script nonce="{{ $cspNonce }}">
// Vertrauenswürdiges Skript
</script>
API-Sicherheit
Ratelimiting
// app/Http/Kernel.php
'api' => [
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
// app/Providers/RouteServiceProvider.php
protected function configureRateLimiting(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
// Spezifische Limits für sensible Endpunkte
RateLimiter::for('auth', function (Request $request) {
return Limit::perMinute(5)->by($request->ip()); // Login-Versuche
});
RateLimiter::for('webhook', function (Request $request) {
return Limit::perMinute(100)->by($request->ip());
});
}
// Routenverwendung
Route::middleware('throttle:auth')->post('/login', [AuthController::class, 'login']);
CORS-Konfiguration
// config/cors.php (Laravel 9+) / config/cors.php (Laravel 7-8)
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'], // Oder spezifisch: ['GET', 'POST', 'PUT', 'DELETE']
'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')], // NIE ['*'] in Produktion
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true, // Für Sanctum-Authentifizierung
];
Sichere Deployment-Konfiguration
Umgebungsvariablen
# .env (Produktion)
APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:... # Ausgeführt: php artisan key:generate
# Datenbank
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=production_db
DB_USERNAME=app_user
DB_PASSWORD=starkes_passwort
# Sitzung
SESSION_DRIVER=redis
SESSION_LIFETIME=120
SESSION_SECURE_COOKIE=true
# Sanctum
SANCTUM_STATEFUL_DOMAINS=app.example.com
SESSION_DOMAIN=.example.com
# Mail (mit API-Token, nicht Plain-Text-Passwort)
MAIL_MAILER=ses
[email protected]
# Redis (falls verwendet)
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
Verzeichnisberechtigungen
# Setzen Sie restriktive Berechtigungen für sensible Dateien
chmod 600 .env
chmod 640 config/app.php
chmod 640 storage/logs/*.log
# storage und bootstrap/cache müssen für den Webserver beschreibbar sein
chmod -R 775 storage bootstrap/cache
chown -R www-data:www-data storage bootstrap/cache
# NIE public-Ordner mit Schreibberechtigungen ausstatten, es sei denn, es ist notwendig (Datei-Uploads)
chmod 755 public
Deployment-Checks
// Pre-Deployment-Validierung in CI/CD
// tests/Feature/DeploymentSecurityTest.php
it('hat Debug-Modus deaktiviert', function () {
expect(config('app.debug'))->toBeFalse();
});
it('hat APP_KEY gesetzt', function () {
expect(config('app.key'))->not->toBeEmpty();
});
it('erzwingt HTTPS in Produktion', function () {
$response = $this->get('/');
expect($response->headers->get('Strict-Transport-Security'))->not->toBeNull();
});
it('hat CSP-Header', function () {
$response = $this->get('/');
$csp = $response->headers->get('Content-Security-Policy');
expect($csp)->toContain("default-src 'self'");
});
it('legt keine sensiblen Daten in Antworten offen', function () {
$response = $this->getJson('/api/user');
$response->assertJsonMissing(['password', 'two_factor_secret']);
});
Zusätzliche Sicherheitsmaßnahmen
Datenvalidierung
// Strenge Validierungsregeln für Eingaben
public function rules(): array
{
return [
'email' => 'required|email:rfc,dns|max:255|unique:users,email',
'phone' => ['required', 'string', 'regex:/^[+]?[0-9\s\-()]{7,15}$/'],
'url' => 'required|url|max:2048',
'file' => 'required|file|mimes:pdf,doc,docx|max:10240', // 10MB
];
}
// Benutzerdefinierte Validierungsregel für sicheres HTML
Validator::extend('safe_html', function ($attribute, $value, $parameters, $validator) {
// Erlaubt nur bestimmte Tags und Attribute
$allowedTags = ['<p>', '<br>', '<strong>', '<em>', '<a>', '<ul>', '<ol>', '<li>'];
$stripped = strip_tags($value, implode('', $allowedTags));
return $stripped === $value;
});
Logging-Sicherheit
// config/logging.php
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['daily', 'slack'],
'ignore_exceptions' => false,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'error'),
'days' => 14, // Logs nach 14 Tagen rotieren
'permission' => 0640, // Restriktive Berechtigungen
],
],
// NIE sensitive Daten in Logs protokollieren
Log::info('Benutzer erstellt', ['user_id' => $user->id]); // SICHER
// Log::info('Passwort geändert', ['password' => $request->password]); // ANGREIFBAR!
Abhängigkeits-Sicherheit
# Regelmäßig auf bekannte Schwachstellen prüfen
composer audit # Laravel 10+
# Oder: composer require --dev roave/security-advisories
# phpinsights für Codestil und Sicherheit
composer require --dev nunomaduro/phpinsights
# npm audit für Frontend-Abhängigkeiten (falls zutreffend)
npm audit
Zusammenfassung der wichtigsten Punkte
- APP_DEBUG muss in Produktion
falsesein - APP_KEY muss gesetzt sein (
php artisan key:generate) - NIE
$guarded = []verwenden - NIE Benutzereingaben ohne Escaping mit
{!! !!}ausgeben - Sitzung bei Login immer regenerieren (
$request->session()->regenerate()) - Passwörter mit bcrypt oder argon2id hashen (mindestens 12 Runden)
- CSRF über
@csrfin allen Formularen einschließen - HTTPS in Produktion erzwingen
- CORS auf vertrauenswürdige Domains beschränken
- Sensitive Daten aus API-Antworten ausschließen
- Ratelimiting für sensible Endpunkte implementieren
- Abhängigkeiten regelmäßig auf Schwachstellen prüfen