Apply when writing code that touches auth, data access, file handling, or external APIs. Stella handles privileged legal data (attorney-client privilege, litigation holds, personal data).
Installs into .claude/skills of the current project.
Are you the author of Conventions Security?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/stella-conventions-security)
---
name: conventions-security
description: "Apply when writing code that touches auth, data access, file handling, or external APIs. Stella handles privileged legal data (attorney-client privilege, litigation holds, personal data)."
---
# Security Conventions
Apply when writing code that touches auth, data access, file
handling, or external APIs. Stella handles privileged legal data
(attorney-client privilege, litigation holds, personal data).
## SOC 2 / ISO 27001 Principles
- **Least privilege** — minimum permissions needed. No wildcard
IAM policies, no admin-by-default.
- **Audit trail** — state-changing operations traceable to an
actor and timestamp. Never silently mutate data.
- **Encryption in transit and at rest** — TLS everywhere, S3 SSE,
no plaintext secrets in code or logs.
- **Input validation at boundaries** — all external input
validated and sanitised before processing.
- **Workspace isolation** — data from one workspace must never
leak to another.
- **Ethical walls** — workspace (matter) boundaries enforce
information barriers for the `member` role at the RLS layer (not
just the UI): a member has zero visibility into workspaces they
are not assigned to via `workspace_members` — no names, no
members, no metadata. **Known limitation:** org `owner`/`admin`
roles carry a deliberate firm-admin override granting access to
every client matter (`client_id IS NOT NULL`) regardless of
assignment, and there is no per-matter screening to wall a
specific admin out of a specific matter. Do not describe admin
access as "absolute confidentiality." Screening a matter from an
admin requires a matter-level exclusion consulted by the RLS
view (`stella_authorized_workspaces`), which is not yet built.
- **Access control** — every endpoint enforces auth and
authorisation. No "internal-only" endpoints without guards.
- **Dependency hygiene** — minimal, pinned, audited.
- **Logging without leaking** — never log secrets, tokens, PII,
or document contents.
- **Change management** — all changes go through PR review. No
direct commits to main.
- **Data retention** — when data is deleted, it is actually
deleted (not soft-deleted indefinitely).
## Structural Guardrails
Prefer solutions that make security bugs **structurally impossible**
(compile-time, lint-time) over ones that rely on developer discipline.
### Authorized database boundary
Workspace-scoped handlers use `createSafeHandler`, declare permissions beside
the route, derive `workspaceId: SafeId<"workspace">` from the authorized
context, and access tenant data through `scopedDb`. Never accept ownership IDs
from body/query input or fall back to root `db` because a relation is awkward
to express. Root/system handlers require a documented non-tenant purpose and
tables whose RLS posture denies ordinary application access. Enforced by
`require-safe-route-handlers/require-safe-route-handlers` and
`no-body-ownership-ids/no-body-ownership-ids`.
When an identifier comes from a related row, authorize it in the same query or
transaction that uses it. A prior UI filter, cache lookup, or separate
read-then-write check is not an access-control boundary.
### Server-bound audit events
Actors, workspace/organization ownership, request metadata, and before/after
identifiers come from authenticated server context. Clients may supply a user
action or reason where the domain requires it, but never the authoritative
actor or tenant. Required audit writes participate in the same transaction as
the mutation, or use a durable outbox when the sink is external. Enforced by
`require-audit-on-mutation/require-audit-on-mutation`.
### Workspace status filtering
`resolveAccessibleWorkspaces` returns **all** workspaces (including
`deleting`). The auth macro exposes two fields:
- `activeWorkspaceIds` — excludes `deleting` workspaces (includes
active and archived). Use this for search, chat, MCP, and any
query that builds a workspace allowlist. This is the default;
reach for it first.
- `accessibleWorkspaces` — includes all statuses. Only use in
`workspaceAccessMacro` (which needs the status to return
appropriate HTTP codes). Never pass these IDs as a search/query
allowlist.
If you need workspace IDs for a new feature, use `activeWorkspaceIds`
unless you have an explicit reason to include deleting workspaces
and document that reason in a comment.
### CSV and file exports
Use `escapeCSV` from `@/api/lib/csv` for all CSV cell values. Never
hand-roll CSV escaping; the shared utility handles both delimiter
quoting and spreadsheet formula neutralization (=, +, -, @, tab, CR
prefixes). This prevents CSV injection attacks where user-controlled
values starting with formula characters execute in Excel/LibreOffice.
### Multi-entry-point validation
Validate untrusted input at each entry boundary with the owning shared schema.
Generated MCP capabilities validate the live endpoint's body, params, and query
schemas in `apps/api/src/mcp/capability-tools.ts`; do not repeat that parsing
inside the handler. Native MCP tools and chat registry tools normalize input
through `normalizeObjectInputAtBoundary` before dispatch; any other caller that
bypasses the HTTP route (cron jobs, workers) must apply the same normalizer and
schema before calling the operation. Keep business invariants and
related-resource authorization in the owning operation: an ID's valid shape
does not establish its ownership.
### Filename sanitization
All user-supplied filenames must pass through `sanitizeFilename`
(`@/api/lib/sanitize-filename`) before storage or use in file
operations (ZIP entries, Content-Disposition headers, S3 keys).
The sanitizer strips path separators, traversal sequences, and
dangerous characters. Enforced by
`security-guards/no-raw-filename-write`.
### Cross-org user ID validation
When a handler accepts a `userId` from user input (body, query, or
params) and uses it in a query that returns user data (names, emails,
images), validate org membership first using `validateOrgUserId` from
`@/api/lib/validated-org-user-id`. The returned `ValidatedOrgUserId`
proves the check happened at the type level, making cross-org user ID
injection structurally impossible. For read paths that resolve
userIds stored in the database (not from user input), scope the user
query with an `innerJoin` on the `member` table filtered by
`session.activeOrganizationId`; enforced by
`security-guards/no-unscoped-user-query`.
### CI workflow permissions
GitHub Actions workflows must declare the **minimum** permissions
needed. Never use `permissions: write-all`. For PR-triggered
workflows, scope to `contents: read` + `pull-requests: write`.
Pin third-party actions to commit SHAs, not mutable tags. For
SBOM/provenance, use the shared `stella/.github` reusable
workflows which handle pinning, checksums, and PR-based updates.