diff --git a/CHANGELOG.md b/CHANGELOG.md index 5102b5375c..11264ebf0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 2.3.8 (2026-11-06) + +### Security + +* **Instance isolation**: an instance's API key could act on other instances of the same installation + ([#2435](https://github.com/evolution-foundation/evolution-api/issues/2435)). Every request is now bound to the + instance it was authenticated for. Installations that host more than one instance should upgrade. + # 2.3.7 (2025-12-05) ### Features diff --git a/package-lock.json b/package-lock.json index c45e8fef38..82f8e1c111 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "evolution-api", - "version": "2.3.7", + "version": "2.3.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "evolution-api", - "version": "2.3.7", + "version": "2.3.8", "license": "Apache-2.0", "dependencies": { "@adiwajshing/keyed-db": "^0.2.4", diff --git a/package.json b/package.json index 56e32fcc8a..e7d48b8fda 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "2.3.7", + "version": "2.3.8", "description": "Rest api for communication with WhatsApp", "main": "./dist/main.js", "type": "commonjs", diff --git a/src/api/abstract/abstract.router.ts b/src/api/abstract/abstract.router.ts index e8449a8c8a..6287d858f4 100644 --- a/src/api/abstract/abstract.router.ts +++ b/src/api/abstract/abstract.router.ts @@ -17,6 +17,21 @@ type DataValidate = { const logger = new Logger('Validate'); +const PROTECTED_INSTANCE_FIELDS = ['instanceName', 'instanceId'] as const; + +function sanitizeUntrustedInput(source: Record | undefined): 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)) { + logger.warn(`Ignoring attempt to override protected field "${key}" via untrusted input`); + continue; + } + sanitized[key] = value; + } + return sanitized; +} + export abstract class RouterBroker { constructor() {} public routerPath(path: string, param = true) { @@ -34,9 +49,11 @@ export abstract class RouterBroker { const instance = request.params as unknown as InstanceDto; if (request?.query && Object.keys(request.query).length > 0) { - Object.assign(instance, request.query); + Object.assign(instance, sanitizeUntrustedInput(request.query as Record)); } + // Create has no :instanceName in the path and only passes the guard with the + // global key, so the body is where the new instance's name legitimately comes from. if (request.originalUrl.includes('/instance/create')) { Object.assign(instance, body); }