# Security Review

> Use before committing changes to auth, workspace scoping, channel webhooks, AI tools/MCP, permission settings, or anything handling untrusted channel content in ChatbotX. A repo-specific security checklist covering tenant isolation, prompt injection via channel content, the Bash permission allowlist, and secret handling. Read before security-sensitive work; pair with the global security-reviewer agent for deep dives.

- Skill: `chatbotxio/security-review` (Agent Skill)
- Install (CLI): `npx skillmds@latest add chatbotxio/security-review`
- Raw SKILL.md: https://api.skillmd.com/api/skills/chatbotxio/security-review/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: chatbotxio (https://skillmd.com/u/chatbotxio)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/chatbotxio/security-review

---


# Security Review (ChatbotX)

A focused, repo-specific checklist. For OWASP-depth analysis dispatch the global `security-reviewer` agent; this skill is the ChatbotX-specific surface map and the things that bite here.

## 1. Tenant isolation (the #1 risk in a multi-workspace product)

- Every query that returns workspace data MUST be scoped by `workspaceId`, OR by an id that is provably workspace-bound by construction (trace it).
- Builder APIs go through `workspaceAuthorizedMidddleware` / `workspaceTokenAuthMidddleware` (triple-d — preserved typo). Confirm new oRPC routes use the authorized stack, not an unauthenticated handler.
- RAG/embedding queries: see the `rag-eval` agent. A `sourceId`-only filter with no `workspaceId` predicate is a defense-in-depth gap.
- Repositories/services are the boundary — app/integration code must not reach `db` directly (`.agents/rules/data-access.md`).

## 1b. Workspace-member permission enforcement

The per-member permission jsonb (`WorkspaceMemberPermissions`: `superAdmin`, `analytics`, `flows`, `contacts`, `onlyAssignedContacts`, `emailAndPhone`, `broadcast`, `ecommerce`) is enforced server-side across three layers that MUST stay in sync — a client-only check is bypassable:

- **Route guard:** `requireWorkspacePermission(workspaceId, key)` / `requireContactsAccess` / `resolveGuardedWorkspaceId` in `apps/builder/src/lib/auth/require-workspace-permission.ts` — call `notFound()` on failure. Gate every new page/layout in a mapped segment (`PERMISSION_NAV` in `lib/auth/permission-routes.ts`). Note route groups split URL-equivalent routes (`products`/`(e-commerce)/products`), so both layouts need the guard.
- **Nav filter:** `app-sidebar.tsx` hides items via `hasWorkspacePermission` (and `canAccessContactsSection` for the compound contacts gate).
- **Data scope:** the builder resolves scope once, in the app layer — `requireContactPermissionScope` (or `resolveContactPermissionScope`) turns member permissions into plain `scope`/`accessScope` params (`restrictToAssignedUserId`, `canViewEmailAndPhone`), then passes them into `contactService.list`/`count`/`findDetailOrFail`. The service applies `restrictToAssignedUserId` as a `conversation.assignedUserId` row filter and masks email/phone when denied — it never re-derives scope itself, and never knows whether the caller was a member or a token. The CSV export mirrors this in the worker; `canExportEmailAndPhone` is a **required** job field so it can never fail open.

Invariants: `hasWorkspacePermission` treats missing jsonb keys as **denied** (fail-closed) and `superAdmin` bypasses every gate. `isCommunity()` normalizes stored permissions to full `getSuperAdminPermissions()` (no granular control in CE). `invite`/`update`/`delete` member actions require caller `superAdmin`. The workspace-token contacts surface (`contactService.list` called with no `scope`) is intentionally **unscoped** by member permissions — verify new token surfaces don't leak member-scoped data by calling the same service method the private path uses, with `scope` simply omitted.

## 1c. Workspace API tokens (`docs/developer/workspace-api-tokens.md`)

- Tokens are stored **hash-only** (SHA-256 `tokenHash` in `WorkspaceApiToken`); the sole plaintext-recoverable row is the `isDefault` token backing `{{api_key}}` (AES-GCM `encryptedToken`, AAD-bound to its workspace). Never persist, cache, or log a raw token or its hash — handler context only ever sees the projected `RequestApiToken`.
- Every workspace-token endpoint MUST use `workspaceTokenAuthAPIForScope("<scope>")` — there is no unscoped stack export. `permission: "read_only"` tokens are limited to GET/HEAD in the middleware; mutations additionally pass the owner-quota gate.
- All bearer-credential material comes from `@chatbotx.io/business/workspace-api-token/credentials` (CSPRNG). Flag any token/secret minted from `Math.random()`-backed helpers.
- Minting/revoking tokens requires workspace `superAdmin` (`requireWorkspaceTokenSuperAdmin`) — a granular member must not escalate via a `full` token.

## 2. Prompt injection (untrusted channel content → agent context)

- Customer messages (WhatsApp/Messenger/webchat), uploaded documents, and fetched URLs are **untrusted**. When their content reaches an AI prompt or RAG context, it must be framed as data, not instructions (clear delimiters, "the following is user-provided content").
- Flag raw `content: row.content` passthrough from a context-source adapter into a model prompt (`apps/worker/src/integration/handlers/automated-response/system-tools/context-sources/`, `packages/ai/`).

## 3. Tool / permission allowlist (`.claude/settings.local.json`)

- This file is git-ignored and local — but it is live in every agent session.
- **Never** put credential literals (`PGPASSWORD`, `DATABASE_URL` with a real password) inside `Bash(...)` allow-patterns. Use env indirection.
- **Never** grant wildcard exec: `Bash(pnpm *)`, `Bash(node *)`, `Bash(python3 *)`, `Bash(git *)`. Grant the specific commands you need (`Bash(pnpm lint)`, `Bash(pnpm --filter <x> test)`).
- **Never** auto-approve `cp` of any `.env`, or reads of `~`/`/etc`. Scope filesystem grants to the repo.
- Paths must be relative/repo-local, not machine-absolute, and must match this repo (`ChatbotX01`).

## 4. Secret handling

- No secrets in tracked files. `.env*` is never committed (`.agents/rules/git.md`, `/commit` guardrails).
- If a real credential is found in any file (even gitignored), **rotate it out-of-band** and remove it — being un-committed is not the same as being safe when it's live in agent context.
- MCP server: restrict CORS origins; accept tokens via `Authorization` header, never URL query params (they get logged).

## Stop condition

Produce a findings list (`file:line | risk | severity | fix`) ranked by severity. If a CRITICAL (secret-in-grant, wildcard exec, cross-tenant access, auth bypass) is found, state it first and recommend blocking the commit until fixed. If nothing security-relevant changed, say `SECURITY: not applicable to this diff` and stop — do not invent risks.

