# Security overview

> How critlab isolates untrusted prototypes, protects credentials and limits outbound requests.

Source: https://critlab.app/docs/enterprise/security

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](/docs/self-hosting/architecture) 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-Cookie` as 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 `sandbox` directive so a prototype opened directly is sandboxed too. `frame-ancestors` limits 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 `postMessage` bridge 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-all `robots.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](/docs/enterprise/sso).
- **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](/docs/enterprise/roles).
- **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](/docs/enterprise/audit-log).

## Credentials at rest

- Workspace bucket credentials are encrypted with AES-256-GCM under `STORAGE_SECRET`; integration tokens and webhook URLs under `INTEGRATIONS_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.

> **Open questions**
>
> - **Security contact and disclosure policy.** A public security contact address and a published disclosure policy are not in the product yet.
> - **Certifications.** No certification or penetration-test report is published yet; this page describes the design as implemented, not an audit result.
