diff --git a/src/api/abstract/abstract.router.ts b/src/api/abstract/abstract.router.ts index 3ff633d58b..97061f86b4 100644 --- a/src/api/abstract/abstract.router.ts +++ b/src/api/abstract/abstract.router.ts @@ -17,11 +17,14 @@ const logger = new Logger('Validate'); const PROTECTED_INSTANCE_FIELDS = ['instanceName', 'instanceId'] as const; -function sanitizeUntrustedInput(source: Record | undefined): Record { +function sanitizeUntrustedInput( + source: Record | undefined, + protectedFields: readonly string[] = PROTECTED_INSTANCE_FIELDS, +): Record { if (!source || typeof source !== 'object') return {}; const sanitized: Record = {}; for (const [key, value] of Object.entries(source)) { - if ((PROTECTED_INSTANCE_FIELDS as readonly string[]).includes(key)) { + if (protectedFields.includes(key)) { logger.warn(`Ignoring attempt to override protected field "${key}" via untrusted input`); continue; } @@ -47,11 +50,16 @@ export abstract class RouterBroker { const instance = request.params as unknown as InstanceDto; if (request?.query && Object.keys(request.query).length > 0) { - Object.assign(instance, sanitizeUntrustedInput(request.query as Record)); + // /instance/fetchInstances has no ":instanceName" in its path either: it is a lookup route + // whose filter (instanceName or instanceId) legitimately arrives in the query string. + const queryProtectedFields = request.originalUrl.includes('/instance/fetchInstances') ? [] : undefined; + Object.assign(instance, sanitizeUntrustedInput(request.query as Record, queryProtectedFields)); } if (request.originalUrl.includes('/instance/create')) { - Object.assign(instance, sanitizeUntrustedInput(body)); + // /instance/create has no ":instanceName" in its path, so the name can only come from the + // body and must be allowed through. instanceId stays protected: it is server-generated. + Object.assign(instance, sanitizeUntrustedInput(body, ['instanceId'])); } Object.assign(ref, body);