fix(security): audit remediation batch A — validation, rate limiting, sessions
CI / backend (pull_request) Successful in 53s
CI / frontend (pull_request) Successful in 15s
Security / secrets (pull_request) Successful in 3s
Security / dependencies (pull_request) Successful in 55s

Implements AUDIT_REPORT.md items H-1, H-2, M-1, M-4, M-6, L-3:
- H-1: enable FluentValidation auto-validation — the registered validators (incl.
  Confirmed-required-for-destructive) now actually execute; invalid DTOs 400 at the
  boundary instead of reaching services.
- H-2: ASP.NET Core rate limiting — global per-user/per-IP fixed window (300/min
  default) + stricter 'auth' (10/min) and 'expensive' (20/min: export, unsubscribe,
  AI) policies; config-driven; 429 with no queue.
- M-1: absolute session lifetime (30d default) — an issued-at stamp set at sign-in
  and checked in OnValidatePrincipal, so a stolen cookie can no longer slide-renew
  forever. Pre-existing sessions re-login once.
- M-4: remove the guessable default DB password from appsettings; startup fails fast
  with a clear message when the connection string has no password (compose/staging
  inject the real one).
- M-6: matching tenant query filter on EmailLabel (via Email navigation) — clears
  the long-standing EF boot warning and closes the join-row leak window.
- L-3: SMTP skip-notice logging downgraded to Debug (recipient address is PII-ish).

Tests: 6 new (400-on-invalid x2, 429 auth rate limit via a test auth scheme,
session-lifetime x3, EmailLabel cross-user invisibility). Full suite: 48/48 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-07-02 03:16:45 +02:00
parent 2056548702
commit 9480033373
10 changed files with 500 additions and 2 deletions
@@ -25,7 +25,7 @@ public class SmtpEmailSender : IEmailSender
{
if (!IsEnabled)
{
_logger.LogInformation("SMTP not configured; skipping email \"{Subject}\" to {To}", subject, toAddress);
_logger.LogDebug("SMTP not configured; skipping email \"{Subject}\" to {To}", subject, toAddress);
return;
}
@@ -53,6 +53,10 @@ public class AppDbContext : DbContext, IAppDbContext
modelBuilder.Entity<MailDomain>().HasQueryFilter(e => CurrentUserId == Guid.Empty || e.UserId == CurrentUserId);
modelBuilder.Entity<Attachment>().HasQueryFilter(e => CurrentUserId == Guid.Empty || e.UserId == CurrentUserId);
modelBuilder.Entity<Label>().HasQueryFilter(e => CurrentUserId == Guid.Empty || e.UserId == CurrentUserId);
// AUDIT M-6: EmailLabel is the required end of a relationship with the filtered Email
// entity; without a matching filter EF warns on boot and joins could surface rows whose
// parent is filtered out. Filter via the Email navigation so the pair is consistent.
modelBuilder.Entity<EmailLabel>().HasQueryFilter(el => CurrentUserId == Guid.Empty || el.Email!.UserId == CurrentUserId);
modelBuilder.Entity<SyncState>().HasQueryFilter(e => CurrentUserId == Guid.Empty || e.UserId == CurrentUserId);
modelBuilder.Entity<AnalyticsAggregate>().HasQueryFilter(e => CurrentUserId == Guid.Empty || e.UserId == CurrentUserId);
modelBuilder.Entity<WidgetLayout>().HasQueryFilter(e => CurrentUserId == Guid.Empty || e.UserId == CurrentUserId);