Skip to main content

Security Overview

Flo's security posture is reviewed with the OWASP Top 10:2025 methodology across three rounds. This page synthesizes the protections that are actually in the codebase: authentication, rate limiting, headers, secrets at rest, and audit/forensics, followed by the remediation history and the operational rules that keep the posture current.

Authentication protections​

Password login, OTP login, and OIDC share the same cookie session model. The flows are described in Authentication; the security-relevant controls are:

  • Password hashing with BCrypt. The policy is a minimum of 8 characters and a 72-UTF-8-byte maximum (BCrypt truncation), plus an offline blocklist of common and breached passwords that also rejects classic mutations (Password1234, p@ssw0rd!). See PasswordHelper and PasswordBlocklist.
  • Login lockout: 5 failed attempts trigger a 15-minute lockout per account. Counters live in the LoginAttempt table, so blue-green containers and process restarts share one view; stale rows are swept opportunistically and a successful login clears the counter.
  • OTP hardening: 6-digit codes, 10-minute expiry, 3 attempts per code, 30 s resend cooldown, constant-time comparison, hashed at rest, and identical responses for known and unknown emails (no enumeration).
  • Token storage: password-reset and email-confirmation tokens are stored as hashes, not in clear.
  • Session protections: the FloAuth cookie is HttpOnly, Secure, SameSite=Lax and Data-Protection encrypted. On top of the sliding expiry, AuthSessionService enforces an absolute 7-day session lifetime and server-side invalidation (global and per-user). Logout revokes OpenIddict refresh tokens and marks cookie sessions stale; password changes and resets invalidate that user's other sessions.
  • Live authorization: LoginAccessMiddleware re-checks login policy and the user's role claims on every authenticated request, re-issuing the cookie when the database privileges changed. Seeded operator passwords force a password change at first login: the dashboard redirects to the change page and the API answers 403 until the password is rotated.
  • Enumeration mitigations: a dummy BCrypt verify equalizes the missing-account path, and password recovery returns one message for both hit and miss.

Rate limiting​

Rate limiting is enforced at the application level (ASP.NET rate limiter policies). The main policies:

PolicyPartitionLimit
AuthEndpointsLimiterclient IP20/min (login, register, recovery; off in Development)
PublicOtpValidateLimiterclient IP10/min
PublicBrowserLimiterclient IP60/min (public forms, OTP request)
PublicIntegrationLimitertoken hash (or IP)300/min
PaymentWebhookLimiterclient IP120/min
BookingCheckoutLimiter / CommerceCheckoutLimiteruser (or IP)10/min
PresignRateLimiteruser120/min
MediaUploadRateLimitertoken hash (or IP)120/min
PublicApiLimiterfixed window10/min, 2 queued

Rejections return 429 with errors.general.rateLimitExceeded. Partitions are resolved by ClientIpResolver: the CF-Connecting-IP header is honored only when TRUST_CF_CONNECTING_IP is enabled and the immediate peer is inside the configured FORWARDED_PROXIES / FORWARDED_NETWORKS boundary, so a spoofed header cannot mint fresh full-quota partitions. The limiter state is in-process: today Flo runs one backend instance per tenant, and horizontal scaling would require a shared backing store.

nginx.conf.template also defines per-IP limit_req zones, but the production fleet runs Docker + Traefik with the application limiters; nginx is only used by the single-tenant/local Compose path (documented exclusion X-07).

Headers and CSP​

For nginx deployments, nginx.conf.template sets HSTS, a Content-Security-Policy (default-src 'self', script hash allow-list, frame-ancestors 'self'), X-Content-Type-Options, X-Frame-Options: SAMEORIGIN, Referrer-Policy, Permissions-Policy (geolocation, microphone, camera disabled), and X-Robots-Tag: noindex on the API. Locations that declare their own add_header repeat the security set, because nginx drops inherited headers in that case.

The Cloudflare Pages SPA ships Flo.FE/public/_headers with nosniff, X-Frame-Options, Referrer-Policy, and Permissions-Policy. CSP is deferred for the SPA because the login page loads third-party SDKs (Google Identity Services, Apple JS); the plan is to ship it Report-Only first.

API responses default to no-store, the exception middleware returns localized error codes without stack traces, and the server banner is suppressed.

Secrets at rest​

  • Tenant secrets: the CLI encrypts secret values in tenant .env files with AES-256-GCM (authenticated tag, scrypt-derived key), keeping the master.key at 0600. Rotation is handled by Secrets Rotation.
  • Webhook auth tokens are encrypted at rest (ASP.NET Data Protection) and never returned in clear.
  • API tokens are stored as a SHA-256 hash plus an 8-character prefix; presign bindings hold token hashes, not the raw token.
  • OTP codes and reset tokens are hashed at rest; access tokens are encrypted in the OpenIddict store.
  • Backups carry .sha256 sidecars that are verified before a restore; pre-deploy dumps are created per deploy.
  • Supply chain: container images are pinned by digest in CI and the Dockerfile, npm ci --ignore-scripts is used, dependencies are locked, and a weekly SCA workflow runs.

Audit and forensics​

The full surface is described in Settings and Audit. Key properties:

  • The audit trail is append-only: the DbContext rejects updates and deletes of AuditLog rows; entity changes are staged in the same transaction as the change with actor, action, old/new values, and timestamp.
  • Retention defaults to 730 days, configurable between 30 and 3650; anonymization and purge operations append tombstones so the log stays self-describing after redaction. GDPR export and anonymization endpoints are SuperAdmin-only.
  • Auth events (/api/v1/auth-events) record masked server- and client-side login failures; password-login failures and lockouts are recorded through the same seam. The viewer lives at Settings > Accessi and errori.
  • Logs: rolling Serilog files with a SuperAdmin-only viewer, CSV/JSON export with spreadsheet-formula neutralization, query-string token redaction, and masked PII in auth log lines. Requests carry an X-Correlation-ID header for tracing.

OWASP remediation history​

RoundTargetOutcome
1 (2026-09-18)Backend API surface + infra (Flo.BE/**, Dockerfile, compose, nginx, CI)41 findings, 4 critical (anonymous log endpoints, committed Jarvis token, seeded SuperAdmin credentials, log exposure); all fixed and re-verified dynamically
2 (2026-09-18)Deep dive: Flo.FE/src/**, cli/src/**, Flo.SchemaGen/** + replay of round-1 fixes22 findings fixed (FE XSS/returnUrl/upload, CLI argv secrets/backup extraction, SchemaGen permissions) and verified
3 (2026-09-19)Full-repo deep audit on HEAD 1e60ce1 + live anonymized clone40 findings across 10 domains (39 confirmed, 1 partial), 1 critical (committed Strapi token); fixes closed in bugfix/owasp-security-fixes with green suites and 9/9 dynamic proofs

Notable round-3 fixes: OIDC logout revoking refresh tokens and server-side sessions, OTP/reset tokens hashed at rest, password blocklist, masked auth logs and sanitized log sinks, DB-backed lockout shared across containers, webhook token encryption, 7-day absolute session cap, audit tombstones, forwarder trust from FORWARDED_* env, Cloudflare Pages security headers, and digest-pinned deploy images.

Permanent exclusions are tracked in the platform repository (docs/SecurityExclusions.md): seeded SuperAdmin credentials (X-01), Origin as defense-in-depth only for API tokens (X-02), MFA/step-up deferred (X-03), SIEM-layer alerting (X-04), social SDKs without SRI (X-05), Flo.STRAPI out of scope (X-06), and nginx not used in production (X-07). A finding that falls in one of these entries is cited as excluded, not silently reopened.

Operational rules​

  • Keep the ReleaseNotes.md Pre-deploy/Post-deploy checklists current: they are the release runbook (see Release Process).
  • Never deploy around the resource preflight, and never use --force without an explicit per-operation authorization.
  • Rotate secrets on a schedule and immediately after any suspected exposure; flo secrets rotate appends new audit salts without invalidating old hashes.
  • Complete a full DR disarm before tenant maintenance or a MON deploy: lease guards fail closed and block writes without valid authority. See Failover and Disaster Recovery.
  • Monitor the surfaces that carry security signals: auth events, audit trail, app logs, and fleet alerts (Monitoring, Control Plane).
  • Never commit secrets or tokens. A committed credential is a security finding, not a configuration detail.