The vulnerability is a sensitive data exposure caused by an incomplete SQL blacklist in the checkSQL function, located in packages/plugins/@nocobase/plugin-collection-sql/src/server/utils.ts. This function failed to block queries to sensitive PostgreSQL system tables like pg_shadow and information_schema. The execute action, part of the SqlCollection resource, receives user-provided SQL via the /api/sqlCollection:execute endpoint and passes it to the vulnerable checkSQL function for validation. The analysis of the provided patch commits (4aecb60d151a9002004dcf984f63d62f17a6cb45 and 87c548969ce9258dd7f0d9571c9453ae10bc3fc4) confirms this root cause, as the fix involves adding the missing sensitive keywords to the dangerKeywords array within checkSQL. Therefore, during exploitation, a runtime profile would show the SqlCollection.execute action calling the checkSQL function, which would incorrectly allow the malicious query to proceed.
This function implements an incomplete blacklist to validate SQL queries. The vulnerability lies in the fact that it did not block keywords for sensitive system tables like 'pg_shadow' or 'information_schema'. The patch adds these keywords to the 'dangerKeywords' array, confirming this function was the source of the vulnerability.
This function is the API action that receives user-supplied SQL. It calls the vulnerable `checkSQL` function to validate the input before execution. As the entry point for the exploit that passes malicious input to the flawed validation logic, it is a critical part of the vulnerable function chain and would appear in a runtime profile during exploitation.
The blacklist approach is fundamentally incomplete. It attempts to enumerate every dangerous construct but misses entire categories:
PostgreSQL system catalog tables — pg_shadow, pg_authid, pg_roles, pg_stat_activity are not restricted
Application-level sensitive tables — users (containing hashed passwords) can be queried directly
information_schema — full schema enumeration is possible
Schema-qualified variants — even some blocked functions could be bypassed via pg_catalog. prefix (e.g. pg_catalog.pg_read_file may bypass checks in older versions)
The correct approach is an allowlist (whitelist) of permitted tables/schemas, not a blacklist of forbidden keywords.
Steps to Reproduce
Prerequisites: A user account with the admin role (has the pm.data-source-manager.collection-sql ACL snippet).
The admin role in NocoBase is an application-level role — it manages workflows, collections, and UI. It is not a database administrator. Accessing pg_shadow is a PostgreSQL system-level privilege that admins should never have. The checkSQL() function was explicitly created to enforce this boundary; bypassing it breaks the intended security model.
2. Data That Admin Cannot Access Through Normal UI
Even with admin privileges, NocoBase's UI and API do not expose:
pg_shadow (PostgreSQL internal password store)
Raw users.password hashes via standard API responses
Full information_schema enumeration
VUL-2 grants access to all of the above — data the application explicitly chose not to expose.
3. Enables Lateral Movement
The pg_shadow SCRAM-SHA-256 hashes can be subjected to offline dictionary attacks. If cracked, the attacker gains direct PostgreSQL access with the application's DB credentials — bypassing the NocoBase application layer entirely. This enables reading all data in the database (not just what NocoBase exposes), modifying records directly, and accessing data from other schemas.
4. Enables Full Attack Chain When Combined with Other Vulnerabilities
Member user (lowest privilege)
→ VUL-8: Trigger a pre-built RCE workflow (any logged-in user can trigger)
→ VUL-1: RCE reads APP_KEY from process.env
→ Forge JWT with admin role
→ VUL-2: Dump pg_shadow + users.password
→ Crack hashes → full PostgreSQL access
Impacted API Endpoint
POST /api/sqlCollection:execute
Authentication: Required (admin role)
ACL Snippet registered in plugin.ts:
this.app.acl.registerSnippet({
name: `pm.data-source-manager.collection-sql`,
actions: ['sqlCollection:*'], // includes :execute
});
The admin role includes this snippet by default.
Recommended Fixes
Fix 1 (Immediate): Extend the blacklist with system catalog tables
const dangerKeywords = [
// ... existing entries ...
// ADD: PostgreSQL system catalog tables with sensitive data
'pg_shadow',
'pg_authid',
'pg_auth_members',
'pg_stat_activity',
'pg_roles',
// Note: information_schema should also be restricted for non-DBA roles
];
Fix 2 (Recommended): Replace blacklist with schema allowlist
Instead of blocking dangerous keywords, only allow queries against user-defined collection tables:
// Allowlist approach: extract table names from AST and verify against known collections
const allowedTables = await db.getCollectionNames(); // tables created by NocoBase users
const referencedTables = extractTableNames(parsedSQL);
if (!referencedTables.every(t => allowedTables.includes(t))) {
throw new Error('Query references tables outside the allowed scope');
}
Fix 3 (Defense-in-depth): Use a read-only, restricted DB user
The application's DB connection should use a PostgreSQL user that:
Does not have SELECT privilege on pg_shadow or pg_authid
Only has access to the application's own schema (nocobase schema)
This ensures that even if the blacklist is bypassed, the DB user cannot access system catalogs.
Environment
| Field | Value |
|-------|-------|
| NocoBase version | 2.0.59-full |
| Database | PostgreSQL 16.14 |
| Deployment | Docker (nocobase/nocobase:2.0.59-full) |
| Vulnerable file | plugin-collection-sql/src/server/utils.ts — checkSQL() |
| Vulnerable endpoint | POST /api/sqlCollection:execute |
| Auth required | Admin role (pm.data-source-manager.collection-sql snippet) |
Timeline
| Date | Event |
|------|-------|
| 2026-05-29 | Vulnerability discovered via whitebox source code audit of utils.ts |
| 2026-05-29 | Exploit verified on live Docker instance — pg_shadow and users.password dumped |
| 2026-05-29 | Report submitted to maintainers |