This catalog records structural security risks observed during the CMS Nova case study.
Some entries describe baseline issues that have since been remediated; others may still be relevant. Each finding includes a status and is written in a research-friendly format (issue + failure mode).
Status legend:
Open: still present / not fully addressed.Mitigated: reduced risk, but not a complete structural fix.Remediated: fixed (or structurally blocked) in the current repo state.
Critical: high-impact exposure with low barriers and direct compromise potential.High: serious confidentiality or integrity risk that violates intended security policy.Medium: meaningful weakness that increases exploitability, drift, or unsafe maintenance.Low: weakness with limited immediate impact but relevant architectural significance.
- Severity:
High - Status:
Remediated - Files:
src/app/[typePath]/[slug]/page.tsx - Issue (baseline): the route resolved entries without an explicit
status: 'published'filter and did not consistently enforce the intended public-route gate. - Why it matters: draft or internal content may be publicly accessible if the slug is known or guessed.
- Remediation: enforced the
public-typepathsgate and published-only lookups. - Failure mode: global publication policy weakened by local route logic.
- Research link:
Lack of Global Context
- Severity:
High - Status:
Remediated - Files:
src/app/[typePath]/page.tsx,src/app/api/plugins/public-typepaths/route.ts - Issue (baseline): the public page checked
data.successinstead ofdata.enabled, so disabled routes could still remain active. - Why it matters: a policy intended as a kill switch does not reliably disable public exposure.
- Remediation: centralized the gate and enforced
enabledsemantics in shared logic. - Failure mode: policy check implemented, but interpreted incorrectly.
- Research link: local correctness without policy correctness.
- Severity:
High - Status:
Remediated - Files:
src/app/api/content-types/[slug]/entries/route.ts,src/app/api/content-types/[slug]/entries/[id]/route.ts,src/app/api/content-entries/route.ts - Issue (baseline): unauthenticated GET handlers returned content entries without restricting results to published state.
- Why it matters: internal CMS data can be enumerated outside intended public views.
- Remediation: restricted admin-oriented entry APIs behind admin session checks.
- Failure mode: route exposure drift.
- Research link:
Naming BiasandLack of Global Context
- Severity:
High - Status:
Remediated - Files:
src/app/api/plans/route.ts,src/app/api/plans/[id]/route.ts,src/app/api/experiences/route.ts - Issue (baseline): public GET handlers returned plans and experiences without enforcing publication visibility.
- Why it matters: unpublished business data and editorial state become observable through direct API access.
- Remediation: required admin session for inventory APIs and moved handlers behind vertical-owned server paths.
- Failure mode: public/private boundary erosion.
- Research link: local feature convenience overriding system policy.
- Severity:
Medium - Status:
Mitigated - Files:
src/app/api/content-types/route.ts,src/app/api/content-types/[slug]/route.ts - Issue (baseline): content type metadata and template structure were exposed publicly.
- Why it matters: schema discovery lowers the cost of targeted probing and content enumeration.
- Remediation: schema discovery now requires an admin session (or a deliberate public-routing mode for dynamic nav), and returns a minimal shape.
- Failure mode: information disclosure through convenience endpoints.
- Research link: overexposure caused by feature-centric reasoning.
- Severity:
High - Status:
Remediated - Files:
src/app/api/media/route.ts,src/app/api/media/folders/route.ts - Issue (baseline): GET endpoints exposed asset metadata, folder names, and search behavior without authentication.
- Why it matters: internal organization and unpublished assets may become discoverable even if not linked publicly.
- Remediation: restricted admin-oriented media inventory endpoints behind admin session checks.
- Failure mode: metadata overexposure.
- Research link: lack of sensitivity classification for asset infrastructure.
- Severity:
Critical - Status:
Mitigated - Files:
src/app/api/plugins/[id]/route.ts,src/modules/plugins/utils.ts,src/modules/plugins/service.ts - Issue (baseline): plugin configuration and secrets were handled through filesystem-backed state and client caching patterns, collapsing the server/client boundary.
- Why it matters: secrets stored outside a privileged and encrypted persistence layer can be leaked through local files, backups, logs, or accidentally reintroduced into client-facing surfaces.
- Remediation: config is now DB-backed and client responses mask sensitive fields; sensitive config values can be encrypted at rest when
ENCRYPTION_KEYis configured. - Failure mode: server/client boundary collapse.
- Research link: strong evidence for the need for centralized policy enforcement.
Notes:
- A concrete example of this class of failure is a plaintext plugin store containing API keys on disk (even if gitignored).
- Full mitigation requires keeping secrets out of client-visible surfaces and configuring
ENCRYPTION_KEYin production.
- Severity:
Medium - Status:
Remediated - Files:
src/modules/content/server/slug-actions.ts - Issue (baseline):
getSlugHierarchy()did not require a session, while neighboring mutation actions did. - Why it matters: internal taxonomy can be queried from the browser even though the feature sits in an admin workflow.
- Remediation: server actions now require an admin session for reads and mutations.
- Failure mode: inconsistent authorization within the same domain surface.
- Research link: local omission of trust-boundary checks.
- Severity:
High - Status:
Mitigated - Files:
src/app/[typePath]/[slug]/page.tsx - Issue (baseline): rich text from database content was rendered via
dangerouslySetInnerHTMLwithout visible sanitization. - Why it matters: malicious or compromised editor content can execute in visitors' browsers.
- Remediation: added a basic sanitizer (
src/shared/security/html.ts) before public HTML injection. - Failure mode: unsafe content rendering.
- Research link: feature completion prioritized over output safety.
- Severity:
Medium - Status:
Mitigated - Files: multiple route, action, and UI files
- Issue (baseline): raw
console.logandconsole.errorcalls were scattered across public, admin, and API surfaces. - Why it matters: identifiers, user details, route traces, and raw errors may leak through logs.
- Remediation: introduced a centralized server logger with redaction; some legacy
console.*usage remains (especially in UI code). - Failure mode: observability without sanitization policy.
- Research link: local debugging convenience overriding operational security.
- Severity:
High - Status:
Mitigated - Files:
src/app/api/admin/create-first-admin/route.ts - Issue (baseline): the route is intentionally public until setup is complete, but had weak initialization controls.
- Why it matters: on a fresh deployment, whoever reaches the endpoint first may become the administrator.
- Remediation: added rate limiting and optional setup-token enforcement (
NOVA_SETUP_TOKEN) to reduce first-caller takeover risk. - Failure mode: insecure initialization workflow.
- Research link: common AI blind spot around deployment-time trust assumptions.
src/server/auth/session.tsprovides a reusable admin-session helper used across privileged surfaces.src/server/auth/config.tscentralizes Better Auth configuration and session semantics.src/server/observability/logger.tsredacts common sensitive keys and reduces production log noise.src/server/policy/api-visibility.jscodifies “default private” for/api/*.scripts/security/check-api-policy.jsenforces API visibility policy at build/check time.src/lib/encryption.tsprovides AES-GCM helpers for encrypting sensitive fields when configured..gitignoreexcludes.env.- several privileged routes already apply rate limiting.
The project demonstrates why AI-assisted systems need structural verification, not only file-level review.
The highest-risk failures were policy failures (route exposure, authorization drift, secret boundaries), which are hard to catch when changes are made with local context only.
- Replace the basic HTML sanitizer with a strict allowlist-based sanitizer.
- Keep reducing raw server-side
console.*usage in favor of centralized logging. - Extend policy checks beyond
/api/*(e.g., Server Actions + publication invariants). - Treat
NOVA_SETUP_TOKENandENCRYPTION_KEYas required production hardening settings.