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.
- Every API route is authenticated except login/logout.
TestDocumentedEndpointsAreRegistereddrives the real mux and fails when a documented route is missing;TestRouteAuthorizationLevelsBehavioralexercises 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
useURLs are removed. Sanitized SVG remains inline-renderable and is served withX-Content-Type-Options: nosniff; raster attachments retain their normal inline behavior.
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.
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.
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.
- 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.
- 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_SECRETduring 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.