Brush Cyber Atlas by Brush Cyber

WG11 BG · Trust & transparency

Page rendered 2026-04-23 08:07 UTC

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 <script> blocks and on*="…" event handlers in app/templates/ have been moved into vendored files under app/static/js/ (theme-init.js, atlas.js, preview-share.js, section-comments.js, contribute-form.js); templates now use data-action="…", data-confirm="…", and data-disable-on-submit attributes that a single delegated listener in atlas.js translates into behaviour. (Inert <script type="application/json"> data blocks remain in templates such as section.html; the browser does not execute these, and CSP does not gate them.) This let us drop 'unsafe-inline' from script-src — the dominant XSS injection sink. ContentSecurityPolicyMiddleware in app/main.py attaches:

default-src 'self';
script-src 'self' 'unsafe-eval';
style-src  'self' 'nonce-{per-request}' https://fonts.googleapis.com;
font-src   'self' https://fonts.gstatic.com;
img-src    'self' data:;
connect-src 'self' https://fonts.googleapis.com https://fonts.gstatic.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';

Residual risk:

  • 'unsafe-eval' is still required by the standard Alpine.js build, which compiles directive expressions (e.g. x-show="open") with new Function(...). This is a smaller XSS surface than 'unsafe-inline' (no <script> injection, no onclick=…) but still allows eval. Migrating to Alpine's CSP build (which forbids inline expressions and requires named data components) is tracked as a follow-up.
  • 'unsafe-inline' has been removed from style-src (2026-04-21). All style="…" attributes in app/templates/ were refactored to vendored utility classes in app/static/css/inline-utils.css (auto-generated .is001.is195 plus hand-written semantic helpers for status pills, bar fills, tile colors, suggestion swatches, and numeric padding/width variants). The remaining <style> blocks in templates (base.html, dashboard.html, god_mode.html, etc.) carry a per-request nonce="{{ csp_nonce(request) }}" matched by the 'nonce-{token}' source in style-src. The nonce is a 16-byte URL-safe random value generated by the CSP middleware in app/main.py and exposed to Jinja via the csp_nonce global registered in app/templating.py. Injected <style>…</style> or style="…" attributes from attacker-controlled content are now blocked outright.

Even with these residuals, no third-party script origin is allow-listed, so an injected <script src="…"> pointing at an attacker-controlled host is blocked, and an injected <script>foo()</script> 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