Whoobe Docs

Threat model#

Written for security officers and architects who have to judge whether Whoobe CMS can be run in their environment. It lists what is protected, who may attack, where the trust boundaries are, what the realistic threats are, which controls answer them and which risks remain. It is a model of the product as released, not a penetration-test report: evidence is pointed to (finding ids in findings.md, suites in security-test-suite.md), and what no test covers is said so. Architecture background: ../architecture/architecture-overview.md.

1. Scope and assumptions#

In scope: the five services, the two browser applications, the compose files and nginx templates, the operator scripts, and the sites and apps generated from the product.

Assumptions the controls depend on (if one is false, the corresponding risks in section 5 are higher):

  1. One workspace = one organisation. Whoobe is not a shared multi-tenant platform: different companies or legal entities get different workspaces; only divisions of the same entity share one, told apart by roles. Inside a workspace every user with objects:write can change any website and every administrator is fully trusted. A deployment that puts unrelated entities in one workspace is outside this model and its risks are not covered here.
  2. The host and the Docker daemon are trusted; anyone who can run docker exec or read .env.<slug> has every key.
  3. Operators follow the install guide: services on loopback behind a TLS-terminating proxy, TRUST_PROXY=1, fresh secrets generated by provision-workspace.sh, backups encrypted.
  4. Authors are semi-trusted: they may be careless or have a stolen account, but are not assumed to be hostile. Content they write is still treated as untrusted when it is rendered to visitors.
  5. PostgreSQL and the object store are reachable only from the stack.

2. Assets#

Asset Why it matters Where
Administrator and editor accounts, sessions, TOTP secrets control of the site and its integrations users, sessions
Third-party credentials: gateway upstreams, mail providers, AI keys, Figma token abuse of the customer's accounts and budgets database (AES-256-GCM for gateway, mail, migration sources, AI, Figma and push keys; the last three only when INTEGRATION_SECRETS_ENCRYPTION_KEY is set, otherwise clear text - F-055)
Encryption keys and shared secrets decrypt everything above .env.<slug>
Published content and drafts integrity (defacement), confidentiality (unpublished) database
Assets, including private files and form uploads personal data, documents object store
Form submissions personal data of visitors data_objects
Availability of the public site and the editor business continuity all services
Reputation of the sending domain the mail features can be abused to send mail providers

3. Actors#

Actor Capability assumed
Anonymous internet user any request to the proxy; automated scanners; scripts at volume
Visitor with a hostile intent submits forms, uploads files, crafts URLs, tries to read unpublished data
Malicious or compromised author (low privilege) logs in, edits content, uploads assets, configures gateway bindings
Compromised administrator account full application access
Application holding an API client key (or someone who extracted it from an app) calls /client/v1 with that key, at volume, from anywhere its address rule allows
Hostile site on the internet lures a logged-in user's browser to send requests (CSRF/CORS)
Malicious tenant owns another workspace on the same host
Supply-chain attacker a poisoned dependency or base image
Insider with database or backup read access reads whatever is stored in clear

4. Attack surface#

Public: /public/* (read API, form submit, upload approval), /preview/*, /dam-asset/*, PUT /public/upload-data/*, /health, the login and password-reset endpoints, the published sites. Authenticated: 172 permission-guarded and 8 login-only core-api routes, the DAM API, the gateway admin API, the two SPAs. Outbound: gateway upstreams, OpenAPI import, AI endpoints, mail providers, Figma. Operator: the scripts in scripts/, the compose files.

5. Threats, controls, evidence#

Severity is the residual risk after the controls, for a correctly installed workspace.

5.1 Authentication and session#

Threat Controls Evidence Residual
Password guessing, credential stuffing argon2id; 5 failures per address+e-mail and 25 per e-mail lock for 15 minutes; generic error text auth suite in-memory lockout is per replica and reset by restart; password policy is length 8 only - Medium for internet-facing admin; mitigate with MFA and proxy/IP rules
Stolen password optional TOTP; code replay blocked; recovery codes hashed; password reset revokes sessions mfa suite MFA is optional and cannot be enforced - Medium
Session theft (XSS, network, database read) HttpOnly, SameSite=Lax, Secure over HTTPS; HSTS; 7-day fixed lifetime auth, TLS scan session ids are stored as hashes and expired sessions are purged (F-056) - Low
Account recovery abuse reset token hashed, one hour, single use, rate limited auth depends on the security of the mailbox
Default credentials no default password: a random one is generated and logged once; compose refuses to start without database and S3 secrets unit/compose-hardening, F-043 outside Compose the service refuses to start without DB_PASSWORD (F-057) - Low

5.2 Authorization#

Threat Controls Evidence Residual
Missing authorization on a route every route in a contract file checked against 5 identities; drift test fails on new routes authz-matrix (900 checks), route-drift asset-processor and gateway routes are not in the contract (covered by suites and ZAP) - Low
Privilege escalation by a low-privilege author publish needs objects:publish (F-035); user and settings management need their own keys; website import cannot publish without it (F-052) limits-and-publish, authz-matrix function-level permissions exist for the content areas (F-058, fixed: a role limited to pages:edit cannot touch snippets); there is no per-website scoping by design (one workspace per organisation) - Low/Medium
IDOR on ids UUID ids; malformed ids answer 4xx without SQL text public-surface, harness
Cross-tenant access separate databases, buckets, secrets and containers per workspace isolation suite on two stacks (14 tests) a shared host and Docker socket are a shared fate

5.3 Injection and content attacks#

Threat Controls Evidence Residual
SQL injection Drizzle with bound parameters; public filters evaluated in JavaScript, not SQL; error bodies never carry SQL text (F-018) static scan (semgrep), public-surface no SQL fuzzing of every authenticated endpoint - Low
Stored XSS via rich text, tables, form messages, Markdown allow-list sanitizer applied when content is served (no iframes, <style>, id, data-*, handlers, javascript: links); text nodes rendered as text browser/xss-sinks (Chromium), SDK SSR tests (F-045) Studio and generated Next/Nuxt/mobile sites are not browser-tested for the same payloads - Medium (F-008)
Stored XSS via uploaded HTML/SVG active types served as attachment, with Content-Security-Policy: sandbox and nosniff (F-022) uploads suite
Code injection into generated sites option, locale and font values are validated before being written into generated code (F-039) publish suite the generated site is not built by the test (next build/nuxt build) - Medium
Command injection through operator scripts argument validation before any docker, nginx or env-file action; services never import child_process; the generator runs one program with an argument array workspace-scripts unit tests operators-only threat model
Prompt injection in AI features provider keys never returned; per-user throttling limits-and-publish (AI throttle) impact of a malicious prompt on generated content is not tested

5.4 Files#

Threat Controls Evidence Residual
Malicious upload to the DAM (authors) per-file cap, asset count cap, path validation, image pixel cap (100 MP), thumbnails through sharp, private-by-default visibility uploads content of non-image files is not inspected; no antivirus
Visitor uploads in forms off by default per website; allow-list of verifiable types; names generated; first bytes checked against the type; pictures decoded and rewritten; size cut while streaming; signed 10-minute single-use ticket; private storage; staging deleted after 2 hours; approval and body rate limited; daily quota per website; internal attach step needs the service token form-uploads (26 API tests), form-uploads unit, browser spec no antivirus step (planned); text/CSV/zip/Office files are accepted by structure only - Medium where files are opened by staff on workstations
Private file read session required for non-public assets and the private/ area; 404 (not 403) to anonymous; Cache-Control: private uploads, F-024
Hotlinking / resource abuse of public assets none (a CDN mirror is optional) Low

5.5 Outbound requests and integrations#

Threat Controls Evidence Residual
SSRF through gateway upstreams, OpenAPI import, AI endpoints safeFetch refuses link-local/metadata, unspecified, multicast, reserved; loopback off in production; private ranges allowed by default (OUTBOUND_ALLOW_PRIVATE=false for internet-facing installs); host allow-list ssrf suite default allows RFC1918 targets - Medium unless OUTBOUND_ALLOW_PRIVATE=false
Credential theft from the gateway AES-256-GCM at rest, injected server-side, never returned; vendor presets carry no secrets gateway, api-gateway-security guide key and process access = all credentials
Misuse or theft of an API client key keys are random (256 bits), stored as a keyed hash, shown once, revocable, rotatable, optionally limited by address and expiry; each client has explicit scopes (deny by default, matched against the configured route, not the path), a rate limit and a daily quota, and its own log lines; a key is for servers: a request carrying a key and an Origin header (a browser) is refused and logged, and an address list is available; every refusal about what exists answers the same 404; failed attempts lock an address for a minute client-keys-scopes, gateway-limiter unit Forged, replayed or stolen identity-provider token
a script can omit Origin, so the address list is the real control; a key placed in an app is exposed (the documentation says not to; identity-provider tokens for browsers and apps are the next step)
Mail relay abuse via forms notification only for a block an author saved; single plain recipient; 3 mails per hour per address; honeypot; form rate limit (F-053) form-notify unit, API tests
Abuse of the AI budget per-user rate limit, keys per user cost control is the provider's

5.6 Network, browser and infrastructure#

Threat Controls Evidence Residual
CORS reflection / CSRF credentialed CORS only for an allow-list; SameSite=Lax; JSON-only mutations; no state-changing GET cors-csrf-headers (F-011) no CSRF token (see auth section 7) - Low/Medium depending on domain layout
Clickjacking frame-ancestors 'none' (dashboard) / configured ancestors (editor) headers test, ZAP baseline no full CSP on the SPAs (F-014) - Low/Medium: it matters most where many people with different trust levels use the editor; under the one-organisation model the editors are the organisation's own staff
TLS downgrade, weak ciphers TLS 1.2/1.3 only, AEAD ciphers, HSTS one year security:tls on the template (F-049) not yet run against a live host
Exposed database / object store published on 127.0.0.1 only; credentials mandatory compose test (F-043)
Container compromise unprivileged users, pinned base images by digest, production images without package managers (F-020, F-004) security:images (trivy) the main compose file sets no read-only filesystem, capability drop or resource limits (the sandbox overlay docker-compose.sandbox.yml does, and can be applied to production); no seccomp profile - Low
Denial of service per-address limits on public routes and logins, body caps, asset and pixel caps limits-and-publish, rate-limit tests no load testing; single instance per service; limiters are per process (see capacity) - Medium for a public site
Log injection / secret leakage in logs redaction of auth headers, cookies and common secret keys; generic 5xx bodies log review (F-018) form values and e-mail addresses can appear in debug logs

5.7 Supply chain and build#

Threat Controls Evidence Residual
Vulnerable or malicious dependency lockfile, pnpm install --frozen-lockfile, pnpm audit baseline with review dates, Dependabot, gitleaks, semgrep security:static two accepted advisories (xlsx, braces); no SBOM is published and releases are not signed - see supply chain
Poisoned base image images pinned by digest; trivy scan of every service image in CI security:images digests lag behind Debian fixes (patched by explicit apt-get/apk upgrade lines)
Secrets in the repository gitleaks in CI; .env.* ignored; images exclude env files and logs (F-020) static scan F-001: the secrets of an old env file were purged from the history; no running instance uses them

6. Top residual risks (what we would tell a risk committee)#

  1. Third-party credentials: encrypted at rest only when INTEGRATION_SECRETS_ENCRYPTION_KEY is set (F-055, fixed; an installation that never sets it keeps clear text and a log warning). The push provider key used to be served by an unauthenticated route (F-059, fixed): after upgrading, rotate any push key that was saved on an internet-reachable installation.
  2. Authorization model is functional, not per site - roles limit what a person can do (pages, snippets, themes ...), workspace-wide; they do not fence one division's websites from another's. Use separate workspaces (export/import moves a site) for that.
  3. No MFA enforcement and no SSO; password policy is length only.
  4. No full CSP on the admin and editor applications; CSRF relies on SameSite/CORS alone.
  5. Files from visitors are not scanned for malware (planned as an optional step).
  6. Single instance, in-memory throttling, no load test - a determined volumetric attack is a proxy/CDN problem.
  7. Generated sites and the mobile app are not browser-tested for the XSS payloads that the editor is tested with.
  8. Key rotation is manual; no audit log of administrative actions beyond mail log, workflow transitions and content versions.

7. How this model is kept honest#

New surface gets a section here and a test before release (the form-upload feature did: plan, 26 API tests, unit tests, browser spec). Findings go to findings.md with the next id; the status summary lists what is open. A reviewer who disagrees with a "Residual" cell should open a finding rather than edit the cell.

Last updated