Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
19 changes: 18 additions & 1 deletion src/api/abstract/abstract.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@ type DataValidate<T> = {

const logger = new Logger('Validate');

const PROTECTED_INSTANCE_FIELDS = ['instanceName', 'instanceId'] as const;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Filtered fetches return every instance

When a caller supplies instanceName or instanceId to /instance/fetchInstances, sanitizeUntrustedInput removes both filters before InstanceController.fetchInstances receives them, so the controller returns the unfiltered instance listing instead of the requested subset.

Preserve these fields as filters for fetchInstances while preventing them from overriding the authenticated instance on other routes.

Also at src/api/abstract/abstract.router.ts:26-28, src/api/abstract/abstract.router.ts:52-53.

Prompt for AI agents
In `src/api/abstract/abstract.router.ts` at line 20:

**Filtered fetches return every instance**

When a caller supplies `instanceName` or `instanceId` to `/instance/fetchInstances`, `sanitizeUntrustedInput` removes both filters before `InstanceController.fetchInstances` receives them, so the controller returns the unfiltered instance listing instead of the requested subset.

Preserve these fields as filters for `fetchInstances` while preventing them from overriding the authenticated instance on other routes.

Also at `src/api/abstract/abstract.router.ts:26-28`, `src/api/abstract/abstract.router.ts:52-53`.


function sanitizeUntrustedInput(source: Record<string, any> | undefined): Record<string, any> {
if (!source || typeof source !== 'object') return {};
const sanitized: Record<string, any> = {};
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) {
Expand All @@ -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<string, any>));
}

// 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);
}
Expand Down
Loading