🛡️ fix: Enforce MULTI_CONVO and agent ACL checks on addedConvo (#12243)

* 🛡️ fix: Enforce MULTI_CONVO and agent ACL checks on addedConvo

addedConvo.agent_id was passed through to loadAddedAgent without any
permission check, enabling an authenticated user to load and execute
another user's private agent via the parallel multi-convo feature.

The middleware now chains a checkAddedConvoAccess gate after the primary
agent check: when req.body.addedConvo is present it verifies the user
has MULTI_CONVO:USE role permission, and when the addedConvo agent_id is
a real (non-ephemeral) agent it runs the same canAccessResource ACL
check used for the primary agent.

* refactor: Harden addedConvo middleware and avoid duplicate agent fetch

- Convert checkAddedConvoAccess to curried factory matching Express
  middleware signature: (requiredPermission) => (req, res, next)
- Call checkPermission directly for the addedConvo agent instead of
  routing through canAccessResource's tempReq pattern; this avoids
  orphaning the resolved agent document and enables caching it on
  req.resolvedAddedAgent for downstream loadAddedAgent
- Update loadAddedAgent to use req.resolvedAddedAgent when available,
  eliminating a duplicate getAgent DB call per chat request
- Validate addedConvo is a plain object and agent_id is a string
  before passing to isEphemeralAgentId (prevents TypeError on object
  injection, returns 400-equivalent early exit instead of 500)
- Fix JSDoc: "VIEW access" → "same permission as primary agent",
  add @param/@returns to helpers, restore @example on factory
- Fix redundant return await in resolveAgentIdFromBody

* test: Add canAccessAgentFromBody spec covering IDOR fix

26 integration tests using MongoMemoryServer with real models, ACL
entries, and PermissionService — no mocks for core logic.

Covered paths:
- Factory validation (requiredPermission type check)
- Primary agent: missing agent_id, ephemeral, non-agents endpoint
- addedConvo absent / invalid shape (string, array, object injection)
- MULTI_CONVO:USE gate: denied, missing role, ADMIN bypass
- Agent resource ACL: no ACL → 403, insufficient bits → 403,
  nonexistent agent → 404, valid ACL → next + cached on req
- End-to-end: both real agents, primary denied short-circuits,
  ephemeral primary + real addedConvo
This commit is contained in:
Danny Avila 2026-03-15 17:12:45 -04:00 committed by GitHub
parent 07d0ce4ce9
commit 8dc6d60750
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 637 additions and 40 deletions

View file

@ -48,14 +48,14 @@ const loadAddedAgent = async ({ req, conversation, primaryAgent }) => {
return null;
}
// If there's an agent_id, load the existing agent
if (conversation.agent_id && !isEphemeralAgentId(conversation.agent_id)) {
if (!getAgent) {
throw new Error('getAgent not initialized - call setGetAgent first');
let agent = req.resolvedAddedAgent;
if (!agent) {
if (!getAgent) {
throw new Error('getAgent not initialized - call setGetAgent first');
}
agent = await getAgent({ id: conversation.agent_id });
}
const agent = await getAgent({
id: conversation.agent_id,
});
if (!agent) {
logger.warn(`[loadAddedAgent] Agent ${conversation.agent_id} not found`);