security: Breadcrumbs ohne Daten an GlitchTip, Übergangsmarke der Session-Sperre entfernt

- ErrorReportScrubber entfernt die Daten aller Breadcrumbs und bereinigt ihre
  Nachrichten; Livewire-Breadcrumbs standardmäßig aus — das SDK hängt dort
  Formularwerte und Properties an (Roadmap 4.9). Bestand in GlitchTip geprüft:
  keine Formular- oder Passwortdaten.
- SessionRevocation::legacyCachedMark() entfernt (Übergang bis v1.6.0)

Changelog: Sicherheit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-10-09 13:35:54 +00:00
parent 29e68330ca
commit d1b9ffdcf1
6 changed files with 49 additions and 18 deletions

View file

@ -5,7 +5,6 @@ namespace App\Services\Auth;
use App\Models\User; use App\Models\User;
use Illuminate\Auth\Events\Login; use Illuminate\Auth\Events\Login;
use Illuminate\Contracts\Session\Session; use Illuminate\Contracts\Session\Session;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str; use Illuminate\Support\Str;
/** /**
@ -90,7 +89,7 @@ class SessionRevocation
*/ */
public function isRevoked(User $user, Session $session): bool public function isRevoked(User $user, Session $session): bool
{ {
$revokedAt = max((int) $user->sessions_revoked_at_ms, $this->legacyCachedMark((int) $user->getKey())); $revokedAt = (int) $user->sessions_revoked_at_ms;
if ($revokedAt === 0) { if ($revokedAt === 0) {
return false; return false;
@ -98,14 +97,4 @@ class SessionRevocation
return (int) $session->get(self::SESSION_KEY, 0) < $revokedAt; return (int) $session->get(self::SESSION_KEY, 0) < $revokedAt;
} }
/**
* Übergang: Bis v1.5.0 lag die Marke nur im Cache, mit Ablauf nach
* `session.lifetime` + 5 Minuten. Sperren aus dieser Zeit sollen über den
* Deploy hinweg gelten. Kann im Release nach v1.6.0 entfallen.
*/
private function legacyCachedMark(int $userId): int
{
return (int) Cache::get('auth.sessions-revoked-at.'.$userId, 0);
}
} }

View file

@ -4,6 +4,7 @@ namespace App\Support\Monitoring;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Sentry\Breadcrumb;
use Sentry\Event; use Sentry\Event;
use Sentry\EventHint; use Sentry\EventHint;
use Sentry\UserDataBag; use Sentry\UserDataBag;
@ -41,6 +42,8 @@ class ErrorReportScrubber
$event->setUser($userId !== null ? UserDataBag::createFromUserIdentifier($userId) : null); $event->setUser($userId !== null ? UserDataBag::createFromUserIdentifier($userId) : null);
$event->setRequest(self::scrubRequest($event->getRequest())); $event->setRequest(self::scrubRequest($event->getRequest()));
$event->setBreadcrumb(array_map(self::scrubBreadcrumb(...), $event->getBreadcrumbs()));
foreach ($event->getExceptions() as $exception) { foreach ($event->getExceptions() as $exception) {
$exception->setValue(self::scrubText($exception->getValue())); $exception->setValue(self::scrubText($exception->getValue()));
} }
@ -58,6 +61,20 @@ class ErrorReportScrubber
return $event; return $event;
} }
public static function scrubBreadcrumb(Breadcrumb $breadcrumb): Breadcrumb
{
$message = $breadcrumb->getMessage();
return new Breadcrumb(
$breadcrumb->getLevel(),
$breadcrumb->getType(),
$breadcrumb->getCategory(),
$message !== null ? self::scrubText($message) : null,
[],
$breadcrumb->getTimestamp(),
);
}
/** /**
* @param array<string, mixed> $request * @param array<string, mixed> $request
* @return array<string, mixed> * @return array<string, mixed>

View file

@ -88,8 +88,10 @@ return [
// Capture Laravel cache events (hits, writes etc.) as breadcrumbs // Capture Laravel cache events (hits, writes etc.) as breadcrumbs
'cache' => env('SENTRY_BREADCRUMBS_CACHE_ENABLED', false), 'cache' => env('SENTRY_BREADCRUMBS_CACHE_ENABLED', false),
// Capture Livewire components like routes as breadcrumbs // Capture Livewire components like routes as breadcrumbs — aus: hängt
'livewire' => env('SENTRY_BREADCRUMBS_LIVEWIRE_ENABLED', true), // Formularwerte und Properties an (Roadmap 4.9); der Scrubber leert
// die Daten aller Breadcrumbs zusätzlich.
'livewire' => env('SENTRY_BREADCRUMBS_LIVEWIRE_ENABLED', false),
// Capture SQL queries as breadcrumbs // Capture SQL queries as breadcrumbs
'sql_queries' => env('SENTRY_BREADCRUMBS_SQL_QUERIES_ENABLED', true), 'sql_queries' => env('SENTRY_BREADCRUMBS_SQL_QUERIES_ENABLED', true),

View file

@ -143,7 +143,7 @@ Wenn der Auftrag lautet „schau in GlitchTip“, „was sagt GlitchTip“ o. Ä
## 5. Regeln, die nicht verletzt werden dürfen ## 5. Regeln, die nicht verletzt werden dürfen
- **Datenschutz (M4):** `send_default_pii` bleibt `false`, `max_request_body_size` bleibt `never`, Log- und Cache-Breadcrumbs bleiben aus, `ErrorReportScrubber` bleibt in `before_send`. Kein Name, keine E-Mail, keine IP an GlitchTip — nur die interne User-ID. Neue Query-Parameter, die mitgehen sollen, nur über `ErrorReportScrubber::AllowedQueryParameters` (und dieselbe Liste in `error-reporting.js` / Umami-Filter). - **Datenschutz (M4):** `send_default_pii` bleibt `false`, `max_request_body_size` bleibt `never`, Log- und Cache-Breadcrumbs bleiben aus, `ErrorReportScrubber` bleibt in `before_send` und entfernt die Daten aller Breadcrumbs; Livewire-Breadcrumbs bleiben aus (sie tragen Formularwerte und Properties, Roadmap 4.9). Kein Name, keine E-Mail, keine IP an GlitchTip — nur die interne User-ID. Neue Query-Parameter, die mitgehen sollen, nur über `ErrorReportScrubber::AllowedQueryParameters` (und dieselbe Liste in `error-reporting.js` / Umami-Filter).
- **Keine variablen Zahlen in Fehlermeldungen**, wenn sie gruppiert werden sollen: GlitchTip gruppiert nach Text. `…after 20002 ms` und `…after 20001 ms` wurden drei getrennte Einträge (Issues 11, 13, 14). Variable Werte in den Kontext, nicht in die Meldung — oder einen Fingerprint setzen wie `ErrorSignal`. - **Keine variablen Zahlen in Fehlermeldungen**, wenn sie gruppiert werden sollen: GlitchTip gruppiert nach Text. `…after 20002 ms` und `…after 20001 ms` wurden drei getrennte Einträge (Issues 11, 13, 14). Variable Werte in den Kontext, nicht in die Meldung — oder einen Fingerprint setzen wie `ErrorSignal`.
- **Vorübergehende Fremdfehler nicht einzeln melden.** Sonst flutet ein Google-Ausfall GlitchTip und verdeckt echte Fehler. - **Vorübergehende Fremdfehler nicht einzeln melden.** Sonst flutet ein Google-Ausfall GlitchTip und verdeckt echte Fehler.
- **Tests melden nie an GlitchTip** (Abschnitt 2). Neue Sentry-Tests nur mit `FakeGlitchTip`. - **Tests melden nie an GlitchTip** (Abschnitt 2). Neue Sentry-Tests nur mit `FakeGlitchTip`.

View file

@ -190,7 +190,7 @@ test('the revocation mark is stored on the account and survives a cache flush',
$this->assertGuest(); $this->assertGuest();
}); });
test('a revocation from before v1.6.0 that only lives in the cache still applies', function () { test('a cache-only mark from before v1.6.0 no longer revokes sessions', function () {
/** @var TestCase $this */ /** @var TestCase $this */
[$user, $otherSessionStamp] = accountWithOtherSession(); [$user, $otherSessionStamp] = accountWithOtherSession();
@ -199,6 +199,6 @@ test('a revocation from before v1.6.0 that only lives in the cache still applies
$this->actingAs($user->fresh()) $this->actingAs($user->fresh())
->withSession([SessionRevocation::SESSION_KEY => $otherSessionStamp]) ->withSession([SessionRevocation::SESSION_KEY => $otherSessionStamp])
->get(route('me.security')) ->get(route('me.security'))
->assertRedirect(route('login')); ->assertSuccessful();
$this->assertGuest(); $this->assertAuthenticatedAs($user);
}); });

View file

@ -6,6 +6,7 @@ use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Livewire\Features\SupportFileUploads\MissingFileUploadsTraitException; use Livewire\Features\SupportFileUploads\MissingFileUploadsTraitException;
use Livewire\Features\SupportLockedProperties\CannotUpdateLockedPropertyException; use Livewire\Features\SupportLockedProperties\CannotUpdateLockedPropertyException;
use Sentry\Breadcrumb;
use Sentry\Event; use Sentry\Event;
use Sentry\ExceptionDataBag; use Sentry\ExceptionDataBag;
use Sentry\SentrySdk; use Sentry\SentrySdk;
@ -110,6 +111,28 @@ test('messages and exception values lose e-mail addresses', function () {
->and($scrubbed?->getUser())->toBeNull(); ->and($scrubbed?->getUser())->toBeNull();
}); });
test('breadcrumbs lose their data, so form values never reach glitchtip', function () {
$event = Event::createEvent();
$event->setBreadcrumb([
new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'livewire', 'Component booted: customer.security', [
'updates' => ['password' => 'Geheim-12345!', 'email' => 'kunde@example.com'],
]),
new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'db.sql.query', 'select * from users where email = info@firma.de', ['executionTimeMs' => 1.2]),
]);
$breadcrumbs = ErrorReportScrubber::beforeSend($event)?->getBreadcrumbs() ?? [];
expect($breadcrumbs)->toHaveCount(2)
->and($breadcrumbs[0]->getMetadata())->toBe([])
->and($breadcrumbs[0]->getMessage())->toBe('Component booted: customer.security')
->and($breadcrumbs[1]->getMetadata())->toBe([])
->and($breadcrumbs[1]->getMessage())->toBe('select * from users where email = [email]');
});
test('livewire breadcrumbs are switched off by default', function () {
expect(config('sentry.breadcrumbs.livewire'))->toBeFalse();
});
test('the test suite never reports to the real glitchtip', function () { test('the test suite never reports to the real glitchtip', function () {
// Ohne diese Absicherung landeten absichtlich provozierte Fehler // Ohne diese Absicherung landeten absichtlich provozierte Fehler
// („Queue weg“, „Datenbank nicht erreichbar“) als Issues in GlitchTip. // („Queue weg“, „Datenbank nicht erreichbar“) als Issues in GlitchTip.