docs/Security/Remediation/FerretDB.md
Status: Design for approval · Owner: xet7 · Related: WeKan.md (the WeKan side and the shared on-disk layout), Snap-Core.
This is the FerretDB (v1, SQLite backend — the wekan/FerretDB fork on main-v1) counterpart of
WeKan.md. It specifies, for the database process:
Crucially, FerretDB does NOT write to any database or file itself. It surfaces a problem to
WeKan (as an error/warning on the operation, or via its log stream), and WeKan records it into the
eventlog collection with a normal Meteor JavaScript query (WeKan.md §3). This keeps
storage DB-agnostic — the feature works identically whether WeKan runs on FerretDB or MongoDB — and
there is no separate FerretDB UI or logger DB; events appear in WeKan's
Admin Panel → Reports → Security / Speed.
FerretDB sits behind WeKan and speaks the MongoDB wire protocol over a local socket; it is not internet-facing, so classic web vulns (SSRF/XSS/CSRF) do not apply here. The FerretDB-relevant categories are:
| General category | What it is here | Auto-remediation |
|---|---|---|
query-abuse | a query/aggregation that would scan or sort an unbounded result set, or a pathological $regex/$where | pushdown to SQLite where safe; bound/timeout; log if it still runs long |
resource-exhaustion | connection-pool starvation, WAL growth, oversized documents, memory blowups | bounded warm pool, busy_timeout, size limits |
injection | building SQL from a collection/field name | deterministic table-name hashing + parameterized SQL (already the design); log any rejected name |
auth / access | (when FerretDB auth is enabled) failed handshakes/authn | count + log; never log credentials |
corruption-guard | orphaned tables, non-finite doubles, schema drift | self-heal (adopt orphan table, sanitize NaN/Inf) — already shipped |
Performance categories (the bigger part): see §5.
FerretDB must not create files or open its own log database. Instead it returns the problem to WeKan, and WeKan saves it:
SQLITE_BUSY exhausted, an orphaned-table adopt, a rejected
name): FerretDB returns a normal MongoDB-wire error/warning to WeKan. WeKan's DB-call sites catch
it and call securityLog.record(...) / speedLog.record(...), which insert into eventlog
(source ferretdb/sqlite.*).internal/util/fsql, FERRETDB_SLOW_QUERY_THRESHOLD) is emitted to FerretDB's log stream; a
thin WeKan-side ingestor (or the operation's own timing) turns it into a speedLog.record(...)
with source:'sqlite.query' and a short statement-shape detail.So every FerretDB event ends up as a document in the same eventlog collection WeKan writes
(rows labelled by source = sqlite…/ferretdb… vs WeKan's localizeAvatar/setAvatarUrl),
shown on WeKan's one Reports screen. No statfs file guard and no separate summary are needed —
storage and counting are the WeKan DB's job (WeKan.md §3, §5).
There is no seclog Go package and no SQLite log file. FerretDB only needs to make each
problem observable to WeKan; the recording is WeKan's job. Concretely:
internal/util/fsql) and, where cheap, add a short structured field
(statement shape, elapsed ms) to make WeKan-side ingestion trivial. Never log document
contents or credentials — only a classification and timing.WeKan maps these to eventlog documents via the category catalog
(models/lib/securityCategories.js) using source:'sqlite.*'.
The database is reached over a local socket by one application, whose driver is a Meteor 3 one. That makes a class of operations interesting by their mere presence: server-side JavaScript evaluation, an aggregation writing its result into a collection, dropping a database, a server-administration command. The driver does not send them. A request that does is either a bug or somebody who has reached the socket and is looking around — and both are worth telling the operator about.
internal/util/canary (paired with tests in the same package) is the FerretDB
half of WeKan.md §12, and it keeps the same three properties:
canary:<id>, which the client parses and the operator reads;
nothing in it says that anything was detected or recorded, so a probe cannot
tell a watched operation from an unimplemented one and route around the watched
ones. A Go test asserts the message contains none of detect, record, log,
alert.The canaries:
| Id | Operations | Why the client never sends it |
|---|---|---|
db.javascript | eval, $where, $function, $accumulator, mapReduce | the driver has no feature that evaluates JavaScript in the database |
db.result-to-collection | $out, $merge | aggregation results are read, never persisted by the database |
db.drop-database | dropDatabase, dropAllDatabases | the application drops collections it owns; dropping the database is an operator action taken with the database's own tools |
db.server-admin | shutdown, setParameter, getParameter, profile, logRotate | these manage the server, not the data |
Check(op) returns nil for everything else, and a Go test pins the ordinary
vocabulary — find, insert, update, aggregate, $match, $group,
$lookup, … — as not tripping. A canary that fires on normal traffic is
worse than no canary: it buries the real ones.
Exactly as §2 describes for every other FerretDB event — the database reports, the client records:
FerretDB WeKan
Check(op) → canary:<id> in the error ──► recordDatabaseProblem()
│ databaseCanaryId() reads the marker
▼
tripCanary('database.canary') ← rate-limited,
│ aggregated,
▼ attributed
eventlog (stream:'security')
│
▼
Admin Panel → Problems → Security
Two things that are deliberate on the WeKan side
(server/lib/databaseProblems.js):
[a-z][a-z0-9.-]{0,60}.database stream. Filing it under "the database said something" would put an
intrusion attempt in the list of things to triage as configuration problems.On MongoDB there is no FerretDB to mark anything, and these operations simply never appear — the WeKan side is inert, and the feature degrades to nothing rather than misbehaving. That is the same property as §2: storage and reporting are WeKan's job, so nothing here depends on which database is underneath.
| Point | Remediation (present/added) | Logged as |
|---|---|---|
internal/backends/sqlite/metadata/registry collection create | adopt orphaned physical table (IF NOT EXISTS) instead of crashing | corruption-guard / (FloppyBleed-style) OrphanTableBleed, remediated |
migration import (legacyToV2 / sanitizeNonFinite) | rewrite bare NaN/Infinity doubles so cards aren't dropped | corruption-guard / NonFiniteBleed, remediated |
| table-name mapping | deterministic hash + parameterized SQL; reject invalid names | injection / generic InjectionBleed, blocked |
| pool checkout | unlimited MaxOpenConns (no starvation) + bounded warm MaxIdleConns | resource-exhaustion, remediated on contention |
PRAGMA busy_timeout handler | contended write waits instead of SQLITE_BUSY | resource-exhaustion, remediated |
Each is a place where FerretDB already does the safe thing (mostly shipped for #6467/#6476/ #6480/#6481); this design adds the event record so the behavior is counted and reportable.
Auto-remediated (already shipped for #6480 and follow-ups):
synchronous(normal) (fewer
fsyncs under WAL), cache_size(-16384), mmap_size(134217728), temp_store(memory),
busy_timeout(30000), journal_mode(wal) — see
internal/backends/sqlite/metadata/pool/uri.go.$in/$regex/ranges) turning WeKan's whole-collection scans into indexed
lookups; bounded warm connection pool; unlimited open connections to avoid cursor starvation.Detected-but-not-auto-fixed → reported to WeKan, which records a speed event (source sqlite.*):
FERRETDB_SLOW_QUERY_THRESHOLD (default 1s) — the existing slow-query
WARN (internal/util/fsql) is surfaced to WeKan, which records a speed event
(category:'slow-query', source:'sqlite.query').category:'no-pushdown').category:'wal-growth' / 'slow-checkpoint').category:'pool-wait').Each carries a short Detail (statement shape — not its bound argument values) and is counted
per category, summarized like WeKan.md §5, and shown in WeKan's Reports → Speed.
Bleed/severity), Detail
truncation/newline-stripping, and the non-blocking drop-on-full-channel path. Negative tests:
malformed event, oversize detail, unwritable directory (no panic, event dropped).internal/util/fsql/slow_test.go) to assert a slow statement also
produces a speed event.go test ./....ferretdb-).eventlog collection, so one admin
screen shows both processes, each row labelled by source (wekan… vs sqlite…/ferretdb…).*Bleed naming is shared; FerretDB adds a few DB-specific generic names
(OrphanTableBleed, NonFiniteBleed, SlowQueryBleed) that do not (yet) have hall-of-fame
pages — the Report shows the general category alongside them.