Seguridad /qr: HTTP Basic Auth con QR_TOKEN dedicado (no secreto en la URL)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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('<h2>401</h2><p>Añade ?key=<FUNNEL_API_KEY> a la URL.</p>');
|
||||
.writeHead(401, {
|
||||
'WWW-Authenticate': 'Basic realm="Reformix QR", charset="UTF-8"',
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
'Referrer-Policy': 'no-referrer',
|
||||
})
|
||||
.end('<h2>401</h2><p>Usuario: cualquiera · Contraseña: el QR_TOKEN.</p>');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -119,7 +131,13 @@ export class WebhookListener implements OnApplicationBootstrap {
|
||||
'</head><body><div>' +
|
||||
cuerpo +
|
||||
'</div></body></html>';
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user