🪪 fix: Enforce Conversation Ownership Checks in Remote Agent Controllers (#12263)

* 🔒 fix: Validate conversation ownership in remote agent API endpoints

Add user-scoped ownership checks for client-supplied conversation IDs
in OpenAI-compatible and Open Responses controllers to prevent
cross-tenant file/message loading via IDOR.

* 🔒 fix: Harden ownership checks against type confusion and unhandled errors

- Add typeof string validation before getConvo to block NoSQL operator
  injection (e.g. { "$gt": "" }) bypassing the ownership check
- Move ownership checks inside try/catch so DB errors produce structured
  JSON error responses instead of unhandled promise rejections
- Add string type validation for conversation_id and previous_response_id
  in the upstream TS request validators (defense-in-depth)

* 🧪 test: Add coverage for conversation ownership validation in remote agent APIs

- Fix broken getConvo mock in openai.spec.js (was missing entirely)
- Add tests for: owned conversation, unowned (404), non-string type (400),
  absent conversation_id (skipped), and DB error (500) — both controllers
This commit is contained in:
Danny Avila 2026-03-16 09:19:48 -04:00 committed by GitHub
parent 951d261f5c
commit 381ed8539b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 218 additions and 7 deletions

View file

@ -292,10 +292,6 @@ const createResponse = async (req, res) => {
// Generate IDs
const responseId = generateResponseId();
const conversationId = request.previous_response_id ?? uuidv4();
const parentMessageId = null;
// Create response context
const context = createResponseContext(request, responseId);
logger.debug(
@ -314,6 +310,23 @@ const createResponse = async (req, res) => {
});
try {
if (request.previous_response_id != null) {
if (typeof request.previous_response_id !== 'string') {
return sendResponsesErrorResponse(
res,
400,
'previous_response_id must be a string',
'invalid_request',
);
}
if (!(await getConvo(req.user?.id, request.previous_response_id))) {
return sendResponsesErrorResponse(res, 404, 'Conversation not found', 'not_found');
}
}
const conversationId = request.previous_response_id ?? uuidv4();
const parentMessageId = null;
// Build allowed providers set
const allowedProviders = new Set(
appConfig?.endpoints?.[EModelEndpoint.agents]?.allowedProviders,