Enterprise
Security overview
How critlab isolates untrusted prototypes, protects credentials and limits outbound requests.
critlab's security model starts from one assumption: every uploaded prototype is untrusted code. It was written by an AI agent or by someone you may never meet, and it runs in your reviewers' browsers. This page summarises how critlab contains it, protects credentials and limits what the server reaches out to. Architecture and trust boundaries shows where each piece runs.
Isolating prototypes
- A separate origin, per version. Prototypes are never served from the app's origin. Each version is served from its own subdomain of a dedicated sandbox zone, so no prototype can read another's cookies, storage or DOM, and none can reach the app's.
- Capability URLs. The subdomain label is a signed, expiring capability that web issues after its own access check (link, password or membership). Tampered, expired and never-issued labels all return the same 404. Labels are re-issued on every page view and never stored.
- No cookies. The sandbox never sets or reads a cookie; a proxy in front of it strips
Set-Cookieas a second layer. - Sandboxed frames and a strict CSP. The viewer frames the prototype in a sandboxed iframe with an opaque origin. The sandbox's CSP names only that version's own origin for connections, allows no third-party scripts, frames or form targets, and carries a
sandboxdirective so a prototype opened directly is sandboxed too.frame-ancestorslimits which sites may frame it to your app and the workspace's verified domains. - External assets are opt-in. A prototype owner may allow external images, fonts, styles and media for one prototype. Scripts, connections, frames, forms and workers never widen, and the sandbox sends no referrer to those hosts.
- A narrow bridge. The comment overlay and the prototype talk only through a token-checked
postMessagebridge with a fixed message list. The prototype cannot make the overlay act on a thread it did not open. - Server-side rendering is sandboxed too. When web renders a prototype headlessly (thumbnails), it frames it on an internal harness page behind a network egress allowlist, never as a top-level page.
- Never indexed. Viewer and sandbox responses carry
X-Robots-Tag: noindex, nofollow, noarchive; both origins serve a deny-allrobots.txt(the public docs and marketing pages are the only exceptions on the app origin).
Identity and access
- No passwords. Sign-in is by emailed one-time code (10 minutes, 5 attempts, stored hashed, rate-limited per address and per IP), by SAML against your IdP, or by Google or Microsoft OIDC if the operator enables them.
- Sessions last 7 days and slide: coming back after a day pushes the expiry out again, so only someone who stays away for a week signs in again. A self-hosting operator can change it with
SESSION_MAX_AGE_DAYS(1 to 365 days). Directory deactivation and signing out end a session at once, whatever its age. - Require SSO makes a workspace reachable only through its IdP, enforced per request. See Single sign-on.
- Roles and folder grants are checked on the server for every request. A guest addressing something outside their grants gets "not found", never a hint that it exists. See Roles and guest access.
- API keys are shown once, stored hashed, act as their creator and are narrowed by scopes. SCIM tokens are stored hashed too.
- Directory deactivation revokes every session of the deactivated person.
- Audit log for membership, access, SSO, SCIM, storage and key changes. See Audit log.
Credentials at rest
- Workspace bucket credentials are encrypted with AES-256-GCM under
STORAGE_SECRET; integration tokens and webhook URLs underINTEGRATIONS_SECRET. Only web ever decrypts them; the sandbox receives bucket credentials from web over an internal, shared-secret route. - A bucket's secret access key is never returned by the API; the access key id is shown masked.
- Licence keys, API keys and tokens never appear in the audit log.
Outbound requests
Every request web makes to a user-supplied URL — IdP metadata, webhooks, a workspace's bucket endpoint, link previews — goes through one guard that:
- resolves the name and refuses private, loopback and link-local addresses, and always refuses cloud metadata addresses, whatever flag is set;
- pins the connection to the address it checked, so DNS cannot change between check and connect;
- follows redirects manually and re-checks every hop;
- caps response size and time.
Private addresses can be allowed per feature for self-hosted networks (STORAGE_ALLOW_PRIVATE_ENDPOINTS, SSO_ALLOW_PRIVATE_METADATA_HOSTS, INTEGRATIONS_ALLOW_ANY_WEBHOOK_HOST), never globally. Link previews can be turned off entirely with LINK_PREVIEWS=off. Nothing phones home.
Data location
On a self-hosted install, all data stays in your Postgres and your object store. On critlab cloud, workspaces carry a data region; an EU workspace's own bucket must be in an EU region unless its owner explicitly allows otherwise.
Custom share domains
On a workspace's own domain, web serves only reviewer pages and the comment API. Sign-in and everything else redirect to the app's own host, so no session cookie is ever set on a domain the customer controls, and certificates are issued only for domains that passed DNS verification.
Reporting a vulnerability
Contact Supermode's security team. An independent penetration test and SOC 2 / ISO 27001 are planned as a separate track.