Owner-only routes wired with an applyPendingChange dispatch helper covering page/project/task/ref/resource/source_doc create/update/delete. Approve and reject emit their own audit_log entries (actions already in the CHECK vocab) so the audit trail is self-contained. Documents a latent bug in security-followups.md: pending_changes.action CHECK constraint blocks 'upsert' / 'add_dependency' / 'remove_dependency' divertToPending paths in refs/resources routes when an agent at suggest tier hits them. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
3.5 KiB
Security Follow-ups for Plan 1
Items flagged by the security review plugin that were deferred at the user's request ("ease up on the security plugin") and are to be addressed in a consolidated pass before Plan 1 is declared complete.
Migration 005 (cross-cutting / polymorphic tables)
The polymorphic shape (entity_type text + entity_id uuid, no composite FK to a
parent) was an approved design decision in docs/superpowers/specs/2026-05-31-void-v2-design.md.
That decision trades referential integrity at the DB layer for one shared join table
across entity types. The plugin's findings are accurate but require revisiting the
design choice — not a one-line fix.
-
[HIGH] entity_tags / entity_links / attachments lack
space_id. Cross-tenant linkage is possible at the DB layer. Mitigation options:- Add
space_idNOT NULL to each polymorphic row; require callers to filter. - Per-type junction tables (e.g.
page_tags,task_links) with real FKs.
- Add
-
[MEDIUM]
tags.name UNIQUEis global, not per-space. Cross-tenant collision creates a side-channel for tag-name discovery. Fix:UNIQUE(space_id, name). -
[MEDIUM] No cascade on parent delete for
attachments/entity_tags. Polymorphic FKs cannot be expressed in standard SQL. Fix options as above, or a deletion trigger per parent type.
Decision pending
Before closing Plan 1, decide whether to:
- (a) Tighten now: add
space_idcolumns + per-type composite FKs toentity_tags,entity_links,attachments, and a per-space UNIQUE ontags. Cost: ~1-2 hrs and invalidates the polymorphic claim in the spec. - (b) Document and defer: accept the tradeoff for Plan 1, gate access at the
REST/MCP layer (where
space_idIS available from the actor's session), and revisit when multi-tenant becomes load-bearing.
Owner-only authn (Plan 1) means there's only one tenant in practice today, so (b) is defensible for the alpha. (a) is the right long-term answer.
Migration 006 (audit + pending_changes) — Plan 2 finding
[HIGH] pending_changes.action CHECK constraint blocks extended actions emitted
by some routes. The table's CHECK accepts only ('create','update','delete'),
but the following routes call divertToPending with actions outside that set:
lib/api/routes/refs.js(POST /refs/upsert) →action: 'upsert'lib/api/routes/resources.js(POST /resources/:id/dependencies) →action: 'add_dependency'lib/api/routes/resources.js(DELETE /resources/:id/dependencies/:dep_id) →action: 'remove_dependency'
Today the owner bearer token always lands in allow tier and these paths never
hit divertToPending. The bug surfaces only when an agent at suggest tier
calls one of these endpoints, in which case the INSERT INTO pending_changes
fails with a CHECK violation and the request 500s.
Mitigation options:
- Widen the CHECK constraint to include
'upsert','add_dependency','remove_dependency'. Add the matching dispatch arms inlib/api/routes/pending_changes.js::applyPendingChange. - Normalize at
divertToPendingentry — collapse'upsert'→'create', reject the dependency variants with405 Method Not Allowed for agents. - Move dependency mutations to owner-only endpoints (remove
requireWriteand the suggest branch) since dependencies are infra-level wiring.
Recommended: (1) for upsert (legitimate suggestion path) and (3) for add_dependency / remove_dependency (infra wiring, owner-only is the right scope).