From a740d088635533191f95f3854a8732275b4435c2 Mon Sep 17 00:00:00 2001 From: Carlos Narro Date: Tue, 9 Jun 2026 16:51:46 +0200 Subject: [PATCH] Seguridad /qr: HTTP Basic Auth con QR_TOKEN dedicado (no secreto en la URL) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review de seguridad: autenticar por query string filtra el secreto (logs, historial, Referer) y usaba la misma FUNNEL_API_KEY que autoriza la API. Ahora /qr usa HTTP Basic (credencial en cabecera), un QR_TOKEN dedicado distinto de FUNNEL_API_KEY, comparación en tiempo constante (timingSafeEqual sobre hashes) y Referrer-Policy: no-referrer. Co-Authored-By: Claude Opus 4.8 --- .../src/webhook/webhook-listener.ts | 44 +++++++++++++------ .../src/whatsapp/whatsapp.service.ts | 2 +- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/mvp/Whatsapp-bot/src/webhook/webhook-listener.ts b/mvp/Whatsapp-bot/src/webhook/webhook-listener.ts index ae7829c..57f74de 100644 --- a/mvp/Whatsapp-bot/src/webhook/webhook-listener.ts +++ b/mvp/Whatsapp-bot/src/webhook/webhook-listener.ts @@ -1,5 +1,6 @@ import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'; import * as http from 'http'; +import * as crypto from 'crypto'; import { ApiClient } from '../api/api-client.service'; const QRImage = require('qrcode'); @@ -21,7 +22,7 @@ export class WebhookListener implements OnApplicationBootstrap { this.logger.log(`Webhook listener en puerto ${port}`); this.logger.log(`WHATSAPP_START → POST /whatsapp-start`); this.logger.log(`WHATSAPP_PDF → POST /whatsapp-pdf`); - this.logger.log(`QR vinculación → GET /qr?key=FUNNEL_API_KEY`); + this.logger.log(`QR vinculación → GET /qr (HTTP Basic, contraseña = QR_TOKEN)`); }); } @@ -75,19 +76,30 @@ export class WebhookListener implements OnApplicationBootstrap { }); } - // Página de vinculación: muestra el QR de Baileys como imagen escaneable. Protegida por ?key=. + // Comparación en tiempo constante (sobre hashes, sin filtrar longitud). + private tokenValido(a: string, b: string): boolean { + if (!a || !b) return false; + const ha = crypto.createHash('sha256').update(a).digest(); + const hb = crypto.createHash('sha256').update(b).digest(); + return crypto.timingSafeEqual(ha, hb); + } + + // Página de vinculación: muestra el QR de Baileys como imagen escaneable. + // Auth por cabecera (HTTP Basic, contraseña = QR_TOKEN), nunca por query string. private async handleQrPage(req: http.IncomingMessage, res: http.ServerResponse) { - const expected = process.env.FUNNEL_API_KEY || ''; - let provided = ''; - try { - provided = new URL(req.url || '', 'http://localhost').searchParams.get('key') || ''; - } catch { - /* url malformada */ - } - if (!expected || provided !== expected) { + const expected = process.env.QR_TOKEN || ''; + const auth = (req.headers['authorization'] as string) || ''; + const pass = auth.startsWith('Basic ') + ? Buffer.from(auth.slice(6), 'base64').toString('utf8').split(':').slice(1).join(':') + : ''; + if (!this.tokenValido(pass, expected)) { res - .writeHead(401, { 'Content-Type': 'text/html; charset=utf-8' }) - .end('

401

Añade ?key=<FUNNEL_API_KEY> a la URL.

'); + .writeHead(401, { + 'WWW-Authenticate': 'Basic realm="Reformix QR", charset="UTF-8"', + 'Content-Type': 'text/html; charset=utf-8', + 'Referrer-Policy': 'no-referrer', + }) + .end('

401

Usuario: cualquiera · Contraseña: el QR_TOKEN.

'); return; } @@ -119,7 +131,13 @@ export class WebhookListener implements OnApplicationBootstrap { '
' + cuerpo + '
'; - res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' }).end(html); + res + .writeHead(200, { + 'Content-Type': 'text/html; charset=utf-8', + 'Cache-Control': 'no-store', + 'Referrer-Policy': 'no-referrer', + }) + .end(html); } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/mvp/Whatsapp-bot/src/whatsapp/whatsapp.service.ts b/mvp/Whatsapp-bot/src/whatsapp/whatsapp.service.ts index ee96085..48e2037 100644 --- a/mvp/Whatsapp-bot/src/whatsapp/whatsapp.service.ts +++ b/mvp/Whatsapp-bot/src/whatsapp/whatsapp.service.ts @@ -136,7 +136,7 @@ export class WhatsappService implements OnModuleInit, OnModuleDestroy { if (qr) { QRCode.generate(qr, { small: true }); - console.log('\n📲 Escanea este QR con WhatsApp (o abre /qr?key=FUNNEL_API_KEY)\n'); + console.log('\n📲 Escanea este QR con WhatsApp (o abre la página /qr, protegida con QR_TOKEN)\n'); this.webhookListener.setQr(qr); }