# Atlas — Threat Model **Project:** Atlas — WG11 Brainstorming Group workspace **Owner:** Brush Cyber (Douglas Brush) for The Sedona Conference WG11 **Scope:** `wg11-sedona.brushcyber.com` **Last reviewed:** 2026-04-17 **Frameworks:** STRIDE, with privacy-by-design overlay --- ## 1. System overview Atlas is a FastAPI + SQLAlchemy + HTMX + Tailwind collaborative workspace used by ~20 named legal professionals to draft a single Sedona Conference WG11 publication. It runs as a single Replit Reserved-VM service behind Replit's mTLS edge with Let's Encrypt TLS, fronted by a custom domain. Authentication is Replit OIDC (PKCE); a magic-link layer provides one-click sign-in from emails. State lives in managed PostgreSQL with daily snapshots. ### 1.1 Trust boundaries | Zone | Examples | Trust | |---|---|---| | **Public internet** | unauthenticated browsers, search bots, Postmark webhook senders | untrusted | | **Authenticated users** | the ~20 allowlisted attorneys + chair + Sedona staff | semi-trusted (have legitimate session, but role-scoped) | | **Privileged users** | `bg_chair`, `fg_lead`, `sedona_staff` | trusted for editorial actions, not for infra | | **Atlas service** | FastAPI process, SQLAlchemy session | trusted | | **Atlas data plane** | managed Postgres, daily snapshots | trusted, encrypted at rest | | **AI providers** | Anthropic, OpenAI, Google | external; no-train contracts; encrypted in transit only | | **SMTP / Postmark** | outbound mail | external; bears recipient PII | ### 1.2 Data classification | Class | Examples | Storage | |---|---|---| | **Identifying** | Name, work email, firm, Replit `sub` | `users` table | | **Confidential drafting** | section bodies, outlines, comments | `sections`, `comments` | | **Operational** | session cookies, magic-link tokens, activity log | session store, `activities` | | **Public** | published outline, trust hub, SBOM | rendered from same DB | --- ## 2. Asset inventory | Asset | Why it matters | |---|---| | **Draft publication text** | Pre-publication legal analysis. Leakage harms WG11 credibility. | | **Member identities & assignments** | Membership of WG11 BG is not strictly secret but unintended disclosure embarrasses the project. | | **Comment threads** | May contain candid internal disagreement. | | **Magic-link tokens** | Bearer auth — anyone with the URL becomes that user. | | **Session cookies** | Same risk as magic-links, but HTTP-only. | | **Admin endpoints** | Bulk email send, weekly nag trigger, citation/QA reports. | | **OIDC client config** | `REPL_ID` is public; secrets handled by Replit OIDC. | | **Postmark token** | Outbound email; can spoof Atlas if leaked. | --- ## 3. STRIDE analysis ### 3.1 Spoofing | Threat | Mitigation | Residual | |---|---|---| | Attacker tries to log in as a member | Replit OIDC required; allowlist enforced by `replit_id`/email match against pre-seeded `users` table; auto-provisioning is disabled | Low | | Attacker forges a magic-link token | `URLSafeTimedSerializer` over `SESSION_SECRET`; HMAC + 7d TTL; verified against `users` row at redemption | Low if `SESSION_SECRET` is strong + rotated | | Sender-spoofing inbound to chair | We do not consume email; only outbound | N/A | | OIDC code interception | PKCE S256 challenge per-session; verifier stored in HTTP-only signed cookie | Low | ### 3.2 Tampering | Threat | Mitigation | Residual | |---|---|---| | Member rewrites another member's section | Edits are role-checked; full immutable audit log in `activities`; section history tracked | Low | | Comment thread modified | Comments are append-only by design | Low | | Bulk-delete of sections via API | Mutating routes require `bg_chair` | Low | | URL parameter manipulation (IDOR) | Section access is gated by role + assignment; no row-level read filter is currently enforced for non-confidential drafting (members can read all sections) — this is intentional | Accepted | | SQL injection | All DB access via SQLAlchemy ORM with bound parameters; no raw f-string SQL | Very low | ### 3.3 Repudiation | Threat | Mitigation | Residual | |---|---|---| | Member denies making a destructive edit | Every meaningful action records an `activities` row with `user_id` + timestamp + detail | Low | | Reminder claimed never sent | `record_activity('reminder_sent' \| 'reminder_failed')` with recipient + Postmark response | Low | | Bulk weekly-nag attribution | `WeeklyNagRun` row records `triggered_by` + week_key + counts | Low | ### 3.4 Information disclosure | Threat | Mitigation | Residual | |---|---|---| | Pre-publication drafts leak via search engines | All drafting routes require login; only `/trust`, `/login` are public | Low | | Magic-link forwarded → other person sees draft | Token is bound to `user_id`; once redeemed it sets a session as that user. Tokens expire in 7 days. **Open issue:** tokens are not single-use — see §6.1. | Medium | | Cron secret leaked via URL logs | Switched to `X-Cron-Secret` header; query string accepted only for back-compat | Low | | Chair tools (`/admin/*`) accessible to non-chairs | `EDITOR_ROLES` gate at every route; verified by tests | Low | | Postmark token leaked | Stored only in env; never logged; never echoed to UI | Low | | AI provider stores prompts | Anthropic, OpenAI, Google contractually exclude API inputs from training; section text is sent only on explicit user action | Low | | Browser console / source map exposes secrets | No secrets in client bundles; HTMX endpoints return rendered HTML only | Low | | Email body discloses content to mail relays | TLS to Postmark; recipient mail providers may store; not unique to Atlas | Accepted | ### 3.5 Denial of service | Threat | Mitigation | Residual | |---|---|---| | Attacker floods reminder send | Chair-only route; quiet-hours throttle; per-user `last_reminded_at` | Low | | AI route abused to burn credits | All AI routes require auth; rate-naturally-limited by single-instance deployment | Medium — no per-user quota yet | | Background scheduler stuck or duplicates emails | Asyncio loop, ticks hourly, idempotent across workers via `weekly_nag_runs.week_key` unique constraint | Low | | Database connection exhaustion | SQLAlchemy pool with default size; sessions closed in `finally` blocks; reviewed by static and architect pass | Low | | Magic-link replay storm | Rate-limit handler not yet present; tokens valid for 7d | Medium | ### 3.6 Elevation of privilege | Threat | Mitigation | Residual | |---|---|---| | Member crafts request to act as chair | `require_user` + role check on every privileged route; no role coming from cookie payload | Low | | Auth bypass enabled in production | `AUTH_BYPASS` env defaults off; `/trust` exposes its real value | Low | | Forged Replit OIDC `id_token` | Token comes directly from Replit over TLS; we trust transport, do not re-verify signature client-side. **Risk** if `replit.com` is impersonated → mitigated by browser TLS cert pinning to public CAs | Low | | Session cookie theft via XSS | Markdown renderer escapes HTML; user content never injected unescaped; CSP enforced with no `'unsafe-inline'` on `script-src` or `style-src` (§6.2) | Low | --- ## 4. Privacy considerations (overlay) * **Data minimisation** — only name, work email, firm affiliation, role, and Replit subject ID stored. No phone, no address, no DOB. * **Right to deletion** — chair can soft-delete a user; full data wipe on project close is contractually committed. * **Cross-border** — service runs in Replit's US infra. All members are US legal professionals; no EU data subjects expected. * **De-identification of style fabric** — accepted text added to the style corpus is stored without `user_id` or `section_id` binding so future stylistic suggestions cannot be attributed back to a single member. * **AI providers** — input only, no training, no human review by provider; surfaced explicitly on `/trust`. --- ## 5. Operational security | Control | Status | |---|---| | HTTPS enforced + HSTS | Yes — Replit edge | | Session cookies HTTP-only, Secure, SameSite-Lax, signed | Yes — itsdangerous | | Secrets via env only, never in code | Yes | | Daily DB snapshots | Yes — managed Postgres | | Dependency audit | `runDependencyAudit` clean (high CVEs patched: Jinja2 ≥3.1.6) | | SAST | `runSastScan` clean | | Privacy / dataflow scan | `runHoundDogScan` clean | | Audit log of mutating actions | `activities` table | | Backups tested | TODO — see §6.3 | --- ## 6. Open issues / planned hardening ### 6.1 Magic-link tokens are not single-use **Risk:** Medium. Forwarded link works for the full 7d window. **Plan:** Add `magic_link_redemptions` table or use a `User.magic_nonce` column; reject re-use after first redemption. Keep TTL at 7d. ### 6.2 Content Security Policy **Status:** Tightened (2026-04-21). htmx, Alpine.js, and Tailwind are vendored under `/static`, so the policy does not allow CDN origins like unpkg.com or cdn.tailwindcss.com. All executable inline `` is blocked outright by the removal of `'unsafe-inline'` from `script-src`. Likewise, injected inline styles cannot exfiltrate data via CSS selectors or rewrite the page chrome, because `'unsafe-inline'` is no longer permitted on `style-src`. ### 6.3 Restore-from-backup drill never run **Risk:** Low until we need it. **Plan:** Document restore-from-snapshot runbook; run a dry restore into a scratch DB once a quarter. ### 6.4 No per-user rate limit on AI endpoints **Risk:** Medium (cost / abuse). **Plan:** Add a token-bucket on `/god-mode/*` AI routes keyed by `user.id`. ### 6.5 No anomaly alerting on bulk send **Risk:** Low. Chair-only and audited but no human alert. **Plan:** Slack / email digest if `reminder_sent` count for a 1h window exceeds N. --- ## 7. Sign-off This threat model is designed to be **read** — by a Steering Committee member who is a privacy lawyer first and a software auditor second. If you read this far and have questions, the chair will route them. — Atlas / Brush Cyber, 2026-04-17