Files
Inboxintel/src/InboxIntel.Infrastructure/DependencyInjection.cs
T
cesnimda 626a9f8454 fix(security): Phase 4 edge hardening + SSRF egress guard
Backend security fixes from the Phase 1 register / Phase 2 roadmap (PR1 + V-01):

- V-01 SSRF: new SafeHttpGuard validates outbound unsubscribe URLs (scheme allowlist
  + DNS-resolve-and-block private/loopback/link-local/ULA/metadata ranges), wired into
  UnsubscribeService; the "unsubscribe" HttpClient now disables auto-redirect so a
  validated external URL can't 3xx into an internal target. +33 unit tests.
- V-04: session cookie SecurePolicy=Always in non-dev (SameAsRequest in dev).
- V-06: UseExceptionHandler/ProblemDetails in prod; Cleanup/Unsubscribe no longer
  echo ex.Message to clients (logged server-side, generic message returned).
- V-08: ForwardedHeaders trusted only from configurable KnownNetworks (default private
  ranges) + ForwardLimit, instead of trusting any client. New ForwardedHeaders config.
- V-09: returnUrl validated with Url.IsLocalUrl (no open redirect via OAuth flow).
- V-10: SearchService clamps Page/PageSize (<=200); Analytics clamps take/days.
- V-11: baseline security headers (nosniff, X-Frame-Options DENY, Referrer-Policy,
  COOP) + HSTS in prod.
- V-13: /app/info discloses only devMode to anonymous callers unless dev mode is on.
- V-12: API container runs as non-root 'app' user (keys dir pre-owned).
- V-03: Postgres + API ports bound to 127.0.0.1; POSTGRES_PASSWORD now required (no
  weak default fallback).

API compatibility preserved (clamps not rejections; error-body shape changes only on
failure paths). No DB migrations. Build + all 33 unit tests green. V-15 (MailKit
NU1902) persists across versions and the SMTP path is default-off — tracked, not bumped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 00:15:04 +02:00

86 lines
3.8 KiB
C#

using InboxIntel.Application.Abstractions;
using InboxIntel.Domain.Enums;
using InboxIntel.Infrastructure.Ai;
using InboxIntel.Infrastructure.Analytics;
using InboxIntel.Infrastructure.Cleanup;
using InboxIntel.Infrastructure.Configuration;
using InboxIntel.Infrastructure.Export;
using InboxIntel.Infrastructure.Gmail;
using InboxIntel.Infrastructure.Notifications;
using InboxIntel.Infrastructure.Persistence;
using InboxIntel.Infrastructure.Search;
using InboxIntel.Infrastructure.Security;
using InboxIntel.Infrastructure.Sync;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace InboxIntel.Infrastructure;
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration config)
{
// EF Core / PostgreSQL
services.AddDbContext<AppDbContext>(opt =>
opt.UseNpgsql(config.GetConnectionString("Postgres"),
npg => npg.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName)));
services.AddScoped<IAppDbContext>(sp => sp.GetRequiredService<AppDbContext>());
// Options
services.Configure<GoogleOAuthOptions>(config.GetSection(GoogleOAuthOptions.SectionName));
services.Configure<GmailSyncOptions>(config.GetSection(GmailSyncOptions.SectionName));
services.Configure<AiOptions>(config.GetSection(AiOptions.SectionName));
services.Configure<SmtpOptions>(config.GetSection(SmtpOptions.SectionName));
services.Configure<DigestOptions>(config.GetSection(DigestOptions.SectionName));
// Security
services.AddSingleton<ITokenProtector, DataProtectionTokenProtector>();
// Gmail
services.AddScoped<GmailClientFactory>();
services.AddScoped<IGmailService, GmailApiService>();
// Background sync queue (singleton) + its worker.
services.AddSingleton<SyncQueue>();
services.AddSingleton<ISyncQueue>(sp => sp.GetRequiredService<SyncQueue>());
services.AddHostedService<SyncQueueWorker>();
// Core services
services.AddScoped<ISyncService, SyncService>();
services.AddScoped<IAnalyticsService, AnalyticsService>();
services.AddScoped<ISearchService, SearchService>();
services.AddScoped<ICleanupService, CleanupService>();
services.AddScoped<IUnsubscribeService, UnsubscribeService>();
services.AddScoped<IExportService, ExportService>();
// Notifications (digest emails)
services.AddScoped<IEmailSender, SmtpEmailSender>();
services.AddScoped<IDigestService, DigestService>();
services.AddHostedService<DigestWorker>();
// HTTP clients
// V-01: do NOT follow redirects — a validated external URL must not be able to
// 3xx-redirect into an internal target after SafeHttpGuard has checked it.
services.AddHttpClient("unsubscribe", c => c.Timeout = TimeSpan.FromSeconds(15))
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AllowAutoRedirect = false });
services.AddHttpClient("ollama");
services.AddHttpClient("openai");
// AI provider selected by configured mode.
var aiMode = config.GetSection(AiOptions.SectionName).GetValue<AiProviderMode>("Mode");
switch (aiMode)
{
case AiProviderMode.LocalOllama: services.AddScoped<IAiProvider, OllamaProvider>(); break;
case AiProviderMode.CloudOpenAi: services.AddScoped<IAiProvider, OpenAiProvider>(); break;
default: services.AddScoped<IAiProvider, NullAiProvider>(); break;
}
services.AddScoped<IAiService, AiService>();
// Background worker (daily incremental sync + aggregate refresh)
services.AddHostedService<GmailSyncWorker>();
return services;
}
}