GHSA-pwqg-q8pg-pp6r Daptin fuzzy search injects unvalidated column name into raw SQL
Summary`processFuzzySearch` in `server/resource/resource_findallpaginated.go:1484` splits the user-supplied `column` parameter by comma and interpolates each segment directly into `goqu.L(fmt.Sprintf("LOWER(%s) LIKE ?", prefix+col))` raw SQL with no column whitelist check. The entry point is `GET /api/<entity>` with `operator=fuzzy` (or `fuzzy_any`, `fuzzy_all`). Any authenticated user — including one who self-registered with no admin involvement — can read the entire database.DetailsAt `resource_findallpaginated.go:1761`, when the operator is `fuzzy`, `fuzzy_any`, or `fuzzy_all`, execution routes to `processFuzzySearch` (line 1763) before `processQueryFilter` (line 1780). `processQueryFilter` is the only path that calls `GetColumnByName` (line 1351), which validates column names against the table schema. The fuzzy branch never reaches that check.Inside `processFuzzySearch` (line 1484), `filterQuery.ColumnName` is split by comma. After `strings.TrimSpace` (line 1486), each segment is routed to a DB-driver-specific function. The injectable sink reached depends on the driver and the `fuzzy_options.fallback_mode` field.**SQLite** (`processFuzzySearchSQLite`, lines 1632–1676) uses `goqu.L` in all code paths — no `fallback_mode` required:`goqu.L(fmt.Sprintf("LOWER(%s) LIKE ?", prefix+col), ...)` — line 1650/1657**PostgreSQL, MySQL, MSSQL** default to `goqu.Ex` (identifier-quoted, not injectable). The `goqu.L` sink is only reached when the attacker supplies a specific `fuzzy_options.fallback_mode` value in the HTTP `query` JSON:PostgreSQL `word_boundary` mode (line 1540): `goqu.L(fmt.Sprintf("%s ~* ?", prefix+col), ...)`MySQL `soundex` mode (line 1598): `goqu.L(fmt.Sprintf("SOUNDEX(%s) = SOUNDEX(?)", prefix+col), ...)`MSSQL `soundex` mode (line 1694): `goqu.L(fmt.Sprintf("DIFFERENCE(%s, ?) >= 3", prefix+col), ...)``fuzzy_options` is deserialized from the HTTP request at line 243 (`json.Unmarshal([]byte(query[0]), &queries)`) — it is fully attacker-controlled.`goqu.L` emits its first argument as a raw SQL literal. The column position uses `%s` string formatting, not a bound parameter.`prefix` is fixed at line 351 as `dbResource.model.GetName() + "."` — for `/api/world` this is `"world."`. Against SQLite, an attacker-supplied column value of `reference_id) OR 1=1 OR LOWER(world.reference_id` expands in the WHERE clause to `LOWER(world.reference_id) OR 1=1 OR LOWER(world.reference_id) LIKE ?`. Against PostgreSQL (where `reference_id` is stored as `bytea`), the `~*` regex operator requires a text-type column; the attack targets a `varchar` column instead (e.g., `table_name`) with an adapted injection template.**Relation to GHSA-rw2c-8rfq-gwfv**: That patch modified `resource_aggregate.go` to fix `/aggregate/:typename`. This vulnerability is in `resource_findallpaginated.go` on the `/api/<entity>` fuzzy path — different file, different endpoint, different operator. The existing patch does not cover this path.**Tested:** SQLite injection dynamically confirmed (boolean-blind extraction, email extracted). PostgreSQL `word_boundary` injection dynamically confirmed (baseline=0 rows, tautology=5 rows, email=`guest@cms.go` extracted via text column). MySQL and MSSQL confirmed by code review; MySQL binary panics on initialization in the test harness (unrelated daptin bug), dynamic verification not performed.**Fix**: Add a `GetColumnByName` whitelist check in `processFuzzySearch` (line 1484) before the comma-split, matching the pattern in `processQueryFilter:1351`. All four DB driver sinks require fixing.PoC**Environment:****poc.py** (Python 3, no dependencies):**Output** (measured on commit `5d32142`, SQLite, macOS arm64):Impact**Attacker precondition**: One valid JWT. Self-signup is enabled by default on a fresh daptin instance — no admin involvement required.**What is impacted**: The full database is readable via boolean-blind extraction, including all tables visible in `sqlite_master` and credential data (emails, bcrypt password hashes) in `user_account`. Extraction rate is approximately 7 HTTP requests per character, making full-database extraction feasible.
CVSS v4.0 score: 7.1