qbit-middleware-mcp performs no table or process permission checks on the tools and resources it exposes. Each tool handler calls QQQ core actions directly, and core actions do not enforce permissions themselves (see QRun-IO/qqq#1061). As a result, an MCP client can query, get, count, insert, update or delete records, and run processes, beyond what the session's user may do. The only limits are the static per-table allowedOperations list and the backend record security locks.
Severity: High (any deployment that gives this server a session with less than full access has a permission bypass)
Priority: high. This repo has no priority labels; the qqq equivalent is priority:p2.
Affected versions:0.1.0-SNAPSHOT (main @ 26745a9, unpublished alpha), used with QQQ 4.1.0-RC.x
Found while designing: the QQQ Assistant qbit. See the design spec in QRun-IO/qqq#1054: the §2.1 component table ("Gaps") and §2.7 "MCP convergence".
There are no PermissionsHelper or permission references anywhere under src/main. Handlers call core actions directly after QContext.init(qInstance, sessionProvider.createSession(exchange)) (AbstractQQQToolHandler L82):
RunProcessToolHandler L81: new RunProcessAction().execute(...). At L65 it sets FrontendStepBehavior.SKIP, so review and confirmation screens are skipped silently.
MCPTableMetaData.exposedFieldNames is only consulted by the schema tool and schema resource (TableSchemaToolHandler L74, TableSchemaResourceHandler L99). It is not applied to query or get results.
Only fixed-user and anonymous session providers exist. The README embedding recipe (README.md ~L201) uses new QSystemUserSession() with full CRUD enabled, though it does carry a warning comment.
Repro sketch
Configure QMCPMetaData with a session provider whose session is a normal user without READ on table X, for example FixedUserMCPSessionProvider with that user. Include QUERY in the table operations.
From an MCP client, call the generated query tool for X.
Expected: permission denied, the same as the HTTP query route gives that user. Actual: records are returned. The same holds for insert/update/delete tools (when enabled) and for process tools for processes the user cannot run.
Impact
An external agent acting as a user gets more rights than that user has in the UI. That includes writes and process runs, with review steps skipped. Examples are Claude Desktop/Code or any other MCP client.
Fields marked as not exposed still appear in query and get output.
Suggested fix design
Deny by default in tool dispatch:
in AbstractQQQToolHandler (and the resource handlers), call PermissionsHelper before every action and refuse unless the check passes:
checkTablePermissionThrowing with READ/INSERT/EDIT/DELETE
checkJoinedTableReadPermissions for filters and joins
checkProcessPermissionThrowing with process values
map QPermissionDeniedException to an MCP tool error
add a test (ArchUnit or unit) asserting that no handler reaches a core action without going through the checked path
Filter list_tables and the advertised tool list to what the session can access. Treat this only as UX: the per-call check is the real control.
Apply exposedFieldNames to query and get results.
Do not SKIP frontend steps by default. Refuse processes that have frontend steps, or return a hand-off URL.
Add a user-session provider that resolves the request's Bearer token, x-api-key or OAuth token through QAuthenticationModuleDispatcher, the same way ExecutorSessionUtils does. Drop the QSystemUserSession recipe from the README.
Longer term: once QRun-IO/qqq#1061 lands, core enforcement adds defense in depth. #1054 §2.7 proposes rebuilding this qbit as a thin adapter over the shared assistant tool kernel.
Tests: for each tool and resource, an allowed session succeeds and a denied session is refused, covering read, write, process and joined-table filters.
## Summary
qbit-middleware-mcp performs **no table or process permission checks** on the tools and resources it exposes. Each tool handler calls QQQ core actions directly, and core actions do not enforce permissions themselves (see QRun-IO/qqq#1061). As a result, an MCP client can query, get, count, insert, update or delete records, and run processes, beyond what the session's user may do. The only limits are the static per-table `allowedOperations` list and the backend record security locks.
- **Severity:** High (any deployment that gives this server a session with less than full access has a permission bypass)
- **Priority:** high. This repo has no priority labels; the qqq equivalent is `priority:p2`.
- **Affected versions:** `0.1.0-SNAPSHOT` (main @ `26745a9`, unpublished alpha), used with QQQ 4.1.0-RC.x
- **Found while designing:** the QQQ Assistant qbit. See the design spec in QRun-IO/qqq#1054: the §2.1 component table ("Gaps") and §2.7 "MCP convergence".
## Evidence (main @ 26745a9)
There are no `PermissionsHelper` or permission references anywhere under `src/main`. Handlers call core actions directly after `QContext.init(qInstance, sessionProvider.createSession(exchange))` ([`AbstractQQQToolHandler` L82](https://github.com/QRun-IO/qbit-middleware-mcp/blob/26745a9090200a4f530ac0eef918f6660d0326f5/src/main/java/com/kingsrook/qbits/middlewaremcp/tools/AbstractQQQToolHandler.java#L82)):
- [`QueryToolHandler` L84](https://github.com/QRun-IO/qbit-middleware-mcp/blob/26745a9090200a4f530ac0eef918f6660d0326f5/src/main/java/com/kingsrook/qbits/middlewaremcp/tools/QueryToolHandler.java#L84): `new QueryAction().execute(...)`
- [`GetToolHandler` L74](https://github.com/QRun-IO/qbit-middleware-mcp/blob/26745a9090200a4f530ac0eef918f6660d0326f5/src/main/java/com/kingsrook/qbits/middlewaremcp/tools/GetToolHandler.java#L74), [`CountToolHandler` L68](https://github.com/QRun-IO/qbit-middleware-mcp/blob/26745a9090200a4f530ac0eef918f6660d0326f5/src/main/java/com/kingsrook/qbits/middlewaremcp/tools/CountToolHandler.java#L68)
- [`InsertToolHandler` L79](https://github.com/QRun-IO/qbit-middleware-mcp/blob/26745a9090200a4f530ac0eef918f6660d0326f5/src/main/java/com/kingsrook/qbits/middlewaremcp/tools/InsertToolHandler.java#L79), [`UpdateToolHandler` L115](https://github.com/QRun-IO/qbit-middleware-mcp/blob/26745a9090200a4f530ac0eef918f6660d0326f5/src/main/java/com/kingsrook/qbits/middlewaremcp/tools/UpdateToolHandler.java#L115), [`DeleteToolHandler` L76](https://github.com/QRun-IO/qbit-middleware-mcp/blob/26745a9090200a4f530ac0eef918f6660d0326f5/src/main/java/com/kingsrook/qbits/middlewaremcp/tools/DeleteToolHandler.java#L76)
- [`RunProcessToolHandler` L81](https://github.com/QRun-IO/qbit-middleware-mcp/blob/26745a9090200a4f530ac0eef918f6660d0326f5/src/main/java/com/kingsrook/qbits/middlewaremcp/tools/RunProcessToolHandler.java#L81): `new RunProcessAction().execute(...)`. At [L65](https://github.com/QRun-IO/qbit-middleware-mcp/blob/26745a9090200a4f530ac0eef918f6660d0326f5/src/main/java/com/kingsrook/qbits/middlewaremcp/tools/RunProcessToolHandler.java#L65) it sets `FrontendStepBehavior.SKIP`, so review and confirmation screens are skipped silently.
- [`RecordResourceHandler` L98](https://github.com/QRun-IO/qbit-middleware-mcp/blob/26745a9090200a4f530ac0eef918f6660d0326f5/src/main/java/com/kingsrook/qbits/middlewaremcp/resources/RecordResourceHandler.java#L98): `new GetAction().execute(...)`
Related gaps in the same code:
- `MCPTableMetaData.exposedFieldNames` is only consulted by the schema tool and schema resource (`TableSchemaToolHandler` L74, `TableSchemaResourceHandler` L99). It is **not** applied to query or get results.
- Only fixed-user and anonymous session providers exist. The README embedding recipe (`README.md` ~L201) uses `new QSystemUserSession()` with full CRUD enabled, though it does carry a warning comment.
## Repro sketch
1. Configure `QMCPMetaData` with a session provider whose session is a normal user **without** READ on table `X`, for example `FixedUserMCPSessionProvider` with that user. Include `QUERY` in the table operations.
2. From an MCP client, call the generated query tool for `X`.
3. Expected: permission denied, the same as the HTTP query route gives that user. Actual: records are returned. The same holds for insert/update/delete tools (when enabled) and for process tools for processes the user cannot run.
## Impact
- An external agent acting as a user gets more rights than that user has in the UI. That includes writes and process runs, with review steps skipped. Examples are Claude Desktop/Code or any other MCP client.
- Fields marked as not exposed still appear in query and get output.
## Suggested fix design
- **Deny by default in tool dispatch:**
- in `AbstractQQQToolHandler` (and the resource handlers), call `PermissionsHelper` before every action and refuse unless the check passes:
- `checkTablePermissionThrowing` with READ/INSERT/EDIT/DELETE
- `checkJoinedTableReadPermissions` for filters and joins
- `checkProcessPermissionThrowing` with process values
- map `QPermissionDeniedException` to an MCP tool error
- add a test (ArchUnit or unit) asserting that no handler reaches a core action without going through the checked path
- Filter `list_tables` and the advertised tool list to what the session can access. Treat this only as UX: the per-call check is the real control.
- Apply `exposedFieldNames` to query and get results.
- Do not SKIP frontend steps by default. Refuse processes that have frontend steps, or return a hand-off URL.
- Add a user-session provider that resolves the request's Bearer token, `x-api-key` or OAuth token through `QAuthenticationModuleDispatcher`, the same way `ExecutorSessionUtils` does. Drop the `QSystemUserSession` recipe from the README.
- Longer term: once QRun-IO/qqq#1061 lands, core enforcement adds defense in depth. #1054 §2.7 proposes rebuilding this qbit as a thin adapter over the shared assistant tool kernel.
- Tests: for each tool and resource, an allowed session succeeds and a denied session is refused, covering read, write, process and joined-table filters.
## Related
- Design spec: QRun-IO/qqq#1054
- Core actions don't enforce permissions: QRun-IO/qqq#1061
- FieldSecurityLock not enforced: QRun-IO/qqq#1062
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
qbit-middleware-mcp performs no table or process permission checks on the tools and resources it exposes. Each tool handler calls QQQ core actions directly, and core actions do not enforce permissions themselves (see QRun-IO/qqq#1061). As a result, an MCP client can query, get, count, insert, update or delete records, and run processes, beyond what the session's user may do. The only limits are the static per-table
allowedOperationslist and the backend record security locks.priority:p2.0.1.0-SNAPSHOT(main @26745a9, unpublished alpha), used with QQQ 4.1.0-RC.xEvidence (main @
26745a9)There are no
PermissionsHelperor permission references anywhere undersrc/main. Handlers call core actions directly afterQContext.init(qInstance, sessionProvider.createSession(exchange))(AbstractQQQToolHandlerL82):QueryToolHandlerL84:new QueryAction().execute(...)GetToolHandlerL74,CountToolHandlerL68InsertToolHandlerL79,UpdateToolHandlerL115,DeleteToolHandlerL76RunProcessToolHandlerL81:new RunProcessAction().execute(...). At L65 it setsFrontendStepBehavior.SKIP, so review and confirmation screens are skipped silently.RecordResourceHandlerL98:new GetAction().execute(...)Related gaps in the same code:
MCPTableMetaData.exposedFieldNamesis only consulted by the schema tool and schema resource (TableSchemaToolHandlerL74,TableSchemaResourceHandlerL99). It is not applied to query or get results.README.md~L201) usesnew QSystemUserSession()with full CRUD enabled, though it does carry a warning comment.Repro sketch
QMCPMetaDatawith a session provider whose session is a normal user without READ on tableX, for exampleFixedUserMCPSessionProviderwith that user. IncludeQUERYin the table operations.X.Impact
Suggested fix design
AbstractQQQToolHandler(and the resource handlers), callPermissionsHelperbefore every action and refuse unless the check passes:checkTablePermissionThrowingwith READ/INSERT/EDIT/DELETEcheckJoinedTableReadPermissionsfor filters and joinscheckProcessPermissionThrowingwith process valuesQPermissionDeniedExceptionto an MCP tool errorlist_tablesand the advertised tool list to what the session can access. Treat this only as UX: the per-call check is the real control.exposedFieldNamesto query and get results.x-api-keyor OAuth token throughQAuthenticationModuleDispatcher, the same wayExecutorSessionUtilsdoes. Drop theQSystemUserSessionrecipe from the README.Related
🤖 Generated with Claude Code