Skip to content

Commit 1230663

Browse files
committed
fix(evohub): inline the hub-id guard at each request sink
The UUID guard lived in assertHubId(), a separate method. It blocks path/URL injection at runtime just fine, but CodeQL's taint analysis does not carry a throwing guard across a function boundary, so js/request-forgery kept flagging every method that interpolates a hub id into the request path — the alerts the guard was added to close stayed open. Same check, now inline at each sink (HUB_ID.test), which is what both the scanner and the runtime accept. Behaviour is unchanged: hub ids are UUIDs (uuid.Parse server-side), so nothing legitimate is rejected. Refs EVO-2098
1 parent 6379772 commit 1230663

1 file changed

Lines changed: 16 additions & 17 deletions

File tree

src/api/integrations/channel/evohub/evohub.client.ts

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ export interface HubChannel {
5353
meta_connection?: HubMetaConnection | null;
5454
}
5555

56+
// SSRF guard: channel/webhook ids do hub são UUID (o hub faz uuid.Parse). O teste roda
57+
// INLINE em cada método que interpola o id no path da request — recusa path/URL injection
58+
// vinda de req.params/req.body em vez de repassá-la ao control-plane. Extrair o guard para
59+
// um método (`assertHubId`) protege igual em runtime, mas a análise de fluxo do CodeQL não
60+
// o reconhece como barreira através da fronteira de função e o js/request-forgery continua
61+
// acusando o sink; o teste inline é o que satisfaz scanner e runtime ao mesmo tempo.
62+
const HUB_ID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
63+
5664
// Webhook do hub (WebhookResponse — webhook.go:116). Só os campos que usamos.
5765
export interface HubWebhookInfo {
5866
id: string;
@@ -138,15 +146,6 @@ export class EvoHubClient {
138146
return this.normalizeChannelList(data);
139147
}
140148

141-
// SSRF guard: channel/webhook ids do hub são UUID (o hub faz uuid.Parse). Validamos
142-
// ANTES de interpolar o id no path da request ao hub — recusa path/URL injection
143-
// vinda de req.params/req.body em vez de repassá-la para o control-plane.
144-
private assertHubId(id: string): void {
145-
if (!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(id)) {
146-
throw new BadRequestException(`invalid hub id: ${id}`);
147-
}
148-
}
149-
150149
// Normaliza a resposta de lista do hub para HubChannel[] (channels|data|array nu).
151150
private normalizeChannelList(data: any): HubChannel[] {
152151
if (Array.isArray(data)) return data;
@@ -162,7 +161,7 @@ export class EvoHubClient {
162161
* server-side; o front NUNCA vê o token.
163162
*/
164163
async getChannel(id: string): Promise<HubChannel> {
165-
this.assertHubId(id);
164+
if (!HUB_ID.test(id)) throw new BadRequestException(`invalid hub id: ${id}`);
166165
const { data } = await this.http.get(`/channels/${id}`);
167166
return data;
168167
}
@@ -179,7 +178,7 @@ export class EvoHubClient {
179178

180179
/** Webhooks já ASSOCIADOS ao canal: GET /api/v1/channels/:id/webhooks → { webhooks, count }. */
181180
async listChannelWebhooks(channelId: string): Promise<HubWebhookInfo[]> {
182-
this.assertHubId(channelId);
181+
if (!HUB_ID.test(channelId)) throw new BadRequestException(`invalid hub id: ${channelId}`);
183182
const { data } = await this.http.get(`/channels/${channelId}/webhooks`);
184183
return this.normalizeWebhookList(data);
185184
}
@@ -199,8 +198,8 @@ export class EvoHubClient {
199198

200199
/** POST /api/v1/webhooks/:id/associate — associa um webhook existente ao canal. */
201200
async associateWebhook(webhookId: string, channelId: string): Promise<void> {
202-
this.assertHubId(webhookId);
203-
this.assertHubId(channelId);
201+
if (!HUB_ID.test(webhookId)) throw new BadRequestException(`invalid hub id: ${webhookId}`);
202+
if (!HUB_ID.test(channelId)) throw new BadRequestException(`invalid hub id: ${channelId}`);
204203
await this.http.post(`/webhooks/${webhookId}/associate`, { channel_id: channelId });
205204
}
206205

@@ -210,13 +209,13 @@ export class EvoHubClient {
210209
* webhook do estado auto-`disabled` (webhook.go:104).
211210
*/
212211
async setWebhookStatus(webhookId: string, status: 'active' | 'inactive'): Promise<void> {
213-
this.assertHubId(webhookId);
212+
if (!HUB_ID.test(webhookId)) throw new BadRequestException(`invalid hub id: ${webhookId}`);
214213
await this.http.put(`/webhooks/${webhookId}/status`, { status });
215214
}
216215

217216
/** PUT /api/v1/webhooks/:id/secret — grava o secret usado para assinar o inbound. */
218217
async setWebhookSecret(webhookId: string, secret: string): Promise<void> {
219-
this.assertHubId(webhookId);
218+
if (!HUB_ID.test(webhookId)) throw new BadRequestException(`invalid hub id: ${webhookId}`);
220219
await this.http.put(`/webhooks/${webhookId}/secret`, { secret });
221220
}
222221

@@ -258,7 +257,7 @@ export class EvoHubClient {
258257
* register-with-own-secret, igual ao provision.
259258
*/
260259
async ensureChannelWebhook(channelId: string, webhookUrl: string): Promise<void> {
261-
this.assertHubId(channelId);
260+
if (!HUB_ID.test(channelId)) throw new BadRequestException(`invalid hub id: ${channelId}`);
262261

263262
const associated = await this.listChannelWebhooks(channelId);
264263
const match = associated.find((w) => w.url === webhookUrl);
@@ -336,7 +335,7 @@ export class EvoHubClient {
336335
* Evolution; 'byo' exige channel_credentials no hub.
337336
*/
338337
async connectToMeta(channelId: string, req: MetaConnectRequest): Promise<MetaConnectResponse> {
339-
this.assertHubId(channelId);
338+
if (!HUB_ID.test(channelId)) throw new BadRequestException(`invalid hub id: ${channelId}`);
340339
const { data } = await this.http.post(`/channels/${channelId}/meta-connect`, req);
341340
return data;
342341
}

0 commit comments

Comments
 (0)