security: Andere Geräte abmelden, IP-weites Login-Limit, dauerhafte Sperrmarke, Passwortregeln, CSP im Melde-Modus
Restliche Phase 4 aus dem Sicherheitsaudit 07.10.2026: - Sicherheitsseite: „Andere Geräte abmelden“ rotiert remember_token und beendet alle anderen Sessions, die aktuelle bleibt (nicht während einer Impersonation). - LoginIpThrottle: 20 Fehlversuche je IP in 15 Minuten über alle Konten. Gezählt über das Failed-Event, damit Livewire-Login und Fortifys POST /login denselben Zähler teilen; Erfolg setzt ihn nicht zurück. - Sperrmarke für Sessions in users.sessions_revoked_at_ms statt nur im Cache — übersteht cache:clear. Die alte Cache-Marke zählt übergangsweise mit und kann im nächsten Release entfallen. - Neue und geänderte Passwörter: mindestens 12 Zeichen, in Produktion zusätzlich uncompromised() (HIBP, k-Anonymität). Bestehende bleiben gültig. - Content-Security-Policy-Report-Only in Produktion; Meldungen über /csp-report nach storage/logs/csp-*.log, je Meldung höchstens einmal pro Stunde, ohne Query und tokenartige Pfadsegmente. Migration: users.sessions_revoked_at_ms (users hat FULLTEXT, 25 MB, Sekunden). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9c4962fbe0
commit
5dfd622141
27 changed files with 601 additions and 39 deletions
65
app/Http/Controllers/CspReportController.php
Normal file
65
app/Http/Controllers/CspReportController.php
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Nimmt CSP-Meldungen der Browser entgegen (ContentSecurityPolicy, Melde-
|
||||
* Modus) und schreibt sie nach storage/logs/csp-*.log. Gleiche Meldungen
|
||||
* landen höchstens einmal pro Stunde im Log. Adressen werden ohne Query und
|
||||
* ohne tokenartige Pfadsegmente gespeichert — signierte Links und
|
||||
* Anmeldelinks gehören nicht ins Log.
|
||||
*/
|
||||
class CspReportController extends Controller
|
||||
{
|
||||
private const MAX_BYTES = 16384;
|
||||
|
||||
public function __invoke(Request $request): Response
|
||||
{
|
||||
$body = $request->getContent();
|
||||
|
||||
if (! config('security.csp.report_only') || $body === '' || strlen($body) > self::MAX_BYTES) {
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
$report = json_decode($body, true)['csp-report'] ?? null;
|
||||
|
||||
if (! is_array($report)) {
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
$entry = [
|
||||
'directive' => mb_substr((string) ($report['effective-directive'] ?? $report['violated-directive'] ?? ''), 0, 60),
|
||||
'blocked' => $this->scrub($report['blocked-uri'] ?? ''),
|
||||
'document' => $this->scrub($report['document-uri'] ?? ''),
|
||||
'source' => $this->scrub($report['source-file'] ?? ''),
|
||||
'line' => (int) ($report['line-number'] ?? 0),
|
||||
'sample' => mb_substr((string) ($report['script-sample'] ?? ''), 0, 40),
|
||||
];
|
||||
|
||||
if (Cache::add('csp-report.'.sha1(json_encode($entry)), true, now()->addHour())) {
|
||||
Log::channel('csp')->warning('CSP-Verstoß (nur gemeldet)', $entry);
|
||||
}
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
private function scrub(mixed $value): string
|
||||
{
|
||||
$value = mb_substr(is_string($value) ? $value : '', 0, 500);
|
||||
$parts = parse_url($value);
|
||||
|
||||
// Schlüsselwörter wie „inline“, „eval“, „data“ unverändert.
|
||||
if (! is_array($parts) || empty($parts['host'])) {
|
||||
return mb_substr(strtok($value, '?#') ?: '', 0, 60);
|
||||
}
|
||||
|
||||
$path = preg_replace('#/[A-Za-z0-9_-]{32,}(?=/|$)#', '/[Filtered]', $parts['path'] ?? '');
|
||||
|
||||
return mb_substr(($parts['scheme'] ?? 'https').'://'.$parts['host'].$path, 0, 200);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue