Skip to content

Security: savantchat/meowiki

Security

docs/SECURITY.md

Security review

Audit date: 2026-08-07. Threat model: an authenticated team member with partial space access, and a leaked API key. The review covered route registration, auth, attachments, request bounds, SQL call sites, and the graph/article APIs. Search and graph SQL implementation files were read-only because they are under active repair.

Enforced controls

  • Every API route is authenticated except login/logout. TestDocumentedEndpointsAreRegistered drives the real mux and fails when a documented route is missing; TestRouteAuthorizationLevelsBehavioral exercises anonymous, read-only, and admin callers against representative read/write/manage routes.
  • Admin routes use RequireAdmin; space routes use explicit read/write/manage levels.

Authorization matrix: /admin/* and space creation/deletion/reindex are admin-only; space metadata/tree/article/revision/diff/attachment reads require read; article and attachment writes require write; space edits and permission administration require manage; article revert requires write; graph and search are authenticated and their store queries are scoped by caller; /auth/me and password change require any valid session or key.

  • Article, revision, diff, and attachment resources use the same space permission check. Hidden resources return 404.
  • Attachment blob paths are derived only from validated UUIDs and SHA-256 hashes; user filenames never participate in filesystem paths. Uploads are size-limited and content-sniffed. Thumbnail widths are bounded and images are never upscaled.
  • SQL uses parameterized pgx arguments. Ordering and numeric query parameters are allow-listed/bounded. Graph depth is capped at 6 and graph limits at 200.
  • Sessions are opaque, database-backed, HttpOnly, SameSite=Lax, Secure under TLS, and expire. API keys are stored hashed and revocation is checked on every request.
  • General/search/write rate buckets are keyed by credential.
  • SVG uploads are sanitized before hashing and storage: script/foreignObject nodes, event-handler attributes, and external/javascript use URLs are removed. Sanitized SVG remains inline-renderable and is served with X-Content-Type-Options: nosniff; raster attachments retain their normal inline behavior.

Findings

Medium — SVG active-content upload sanitization (fixed)

Reproduction before the fix: upload an SVG containing <script>, onload, <foreignObject>, and external or javascript: use URLs, then fetch it. The regression test TestSVGAttachmentIsSanitizedInlineAndNosniff proves those constructs are absent while the response remains image/svg+xml and inline-capable.

Low — global security headers are not yet centralized

Attachment responses now set nosniff; the server does not emit a global CSP or HSTS header. This is an operational hardening gap rather than a demonstrated bypass: terminate TLS at a reverse proxy and add HSTS/CSP there. A future centralized security-header middleware should cover JSON and SPA responses too.

Informational — search concealment remains delegated

The route is authenticated and passes the caller identity to the search store, but the active search repair (337e3f) is in protected files. Reproduction from the production smoke run: create a readable article containing container search sentinel, reindex, then GET /api/v1/search?q=sentinel; response was 200 with total: 0. This is correctness, not an authorization bypass; it must be re-audited after that fix, including hidden-space totals and response shape.

Checked with no finding

  • Path traversal: UUID parsing, hash-only paths, absolute paths, .., NUL bytes, Unicode filenames, and symlinked storage roots do not create a user-derived blob path.
  • Upload limits and MIME sniffing: bounded multipart reads reject oversized payloads; generic paste filenames and client MIME lies are handled by sniffing.
  • SQL injection: reviewed API/store call sites outside the protected search/import files; user values are bound parameters and ordering is allow-listed.
  • Session fixation, inactive-user/session and API-key revocation behavior: covered by auth integration tests; sessions and keys are revalidated against current database state.
  • Denial-of-service bounds: article JSON is capped at 1 MiB, upload at 25 MiB by default, graph depth/limits and search/list limits are bounded. Pathological regex execution is not used by the API.

Operational advice

  • Run behind TLS; set secure cookies and add HSTS/CSP at the proxy.
  • Treat API keys as bearer secrets. Revoke leaked keys immediately, issue replacements, and keep them out of logs. Rotate MEOWIKI_SESSION_SECRET during a maintenance window.
  • Encrypt backup directories at rest, restrict their permissions, and test restores in an isolated database. Do not expose blob storage directly to the internet.
  • Re-run the security tests after every route addition and after the pending search, graph, and importer changes land.

Out of scope: host kernel/container hardening, PostgreSQL CVEs/configuration beyond the application connection, browser extensions, and the implementation in the explicitly protected search/graph/importer files.

There aren't any published security advisories