2 Commits

Author SHA1 Message Date
Edrei Téllez ce1d1396e0 SEC-001: migrar HcaptchaGuard a TurnstileGuard con siteverify Cloudflare.
Bypass solo en desarrollo; pruebas y producción validan header turnstile vía HTTPS nativo.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 10:29:57 -06:00
edreitega e5076603fd Merge pull request 'alinear dev con master (prod)' (#1) from master into dev
Reviewed-on: #1
2026-06-04 18:25:37 -06:00
6 changed files with 86 additions and 55 deletions
-11
View File
@@ -25,7 +25,6 @@
"class-validator": "^0.13.2",
"csvtojson": "^2.0.10",
"dotenv": "^16.0.3",
"hcaptcha": "^0.1.1",
"moment": "^2.29.4",
"mysql2": "^2.3.3",
"nodemailer": "^6.7.8",
@@ -5049,11 +5048,6 @@
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
},
"node_modules/hcaptcha": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/hcaptcha/-/hcaptcha-0.1.1.tgz",
"integrity": "sha512-iMrDmH2VpIEKOrcKWidVjI89FdDKTEdZ7PfPWkP27sTazIIkob8YfdY2ezaufAnWBiUUcvzsn0qF+dyXtBH2Vw=="
},
"node_modules/hexoid": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",
@@ -13436,11 +13430,6 @@
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
},
"hcaptcha": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/hcaptcha/-/hcaptcha-0.1.1.tgz",
"integrity": "sha512-iMrDmH2VpIEKOrcKWidVjI89FdDKTEdZ7PfPWkP27sTazIIkob8YfdY2ezaufAnWBiUUcvzsn0qF+dyXtBH2Vw=="
},
"hexoid": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",
-1
View File
@@ -37,7 +37,6 @@
"class-validator": "^0.13.2",
"csvtojson": "^2.0.10",
"dotenv": "^16.0.3",
"hcaptcha": "^0.1.1",
"moment": "^2.29.4",
"mysql2": "^2.3.3",
"nodemailer": "^6.7.8",
+4 -4
View File
@@ -2,7 +2,7 @@ import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Serealize } from '../interceptors/serialize.interceptor';
import { HcaptchaGuard } from '../guards/hcaptcha.guard';
import { TurnstileGuard } from '../guards/turnstile.guard';
import { AuthService } from './auth.service';
import { LoginAdminDto } from './dto/input/login-admin.dto';
import { LoginOperadorDto } from './dto/input/login-operador.dto';
@@ -15,7 +15,7 @@ export class AuthController {
constructor(private authService: AuthService) {}
@Serealize(AuthTokenOutputDto)
@UseGuards(HcaptchaGuard)
@UseGuards(TurnstileGuard)
@Post('login-admin')
@ApiOperation({ description: 'Login del admin.' })
@ApiBody({
@@ -27,7 +27,7 @@ export class AuthController {
}
@Serealize(AuthTokenOutputDto)
@UseGuards(HcaptchaGuard)
@UseGuards(TurnstileGuard)
@Post('login-operador')
@ApiOperation({ description: 'Login del operador.' })
@ApiBody({
@@ -45,7 +45,7 @@ export class AuthController {
}
@Serealize(AuthTokenOutputDto)
@UseGuards(HcaptchaGuard)
@UseGuards(TurnstileGuard)
@Post('login-usuario')
@ApiOperation({ description: 'Login del usuario.' })
@ApiBody({
-37
View File
@@ -1,37 +0,0 @@
const { verify } = require('hcaptcha');
import {
BadRequestException,
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
InternalServerErrorException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Observable } from 'rxjs';
@Injectable()
export class HcaptchaGuard implements CanActivate {
constructor(private configService: ConfigService) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const secret = this.configService.get<string>('HCAPTCHA_KEY');
const token = context.switchToHttp().getRequest().headers.hcaptcha;
if (this.configService.get<string>('STATE') !== 'produccion') return true;
if (!token)
throw new BadRequestException('No se mando un token de hcaptcha.');
return verify(secret, token)
.then((data) => {
if (data.success === true) return true;
else throw new ForbiddenException('El token de hcaptcha no es válido.');
})
.catch((err) => {
throw new InternalServerErrorException(
'Ocurrió un error con el hcaptcha.',
);
});
}
}
+80
View File
@@ -0,0 +1,80 @@
import {
BadRequestException,
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
InternalServerErrorException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as https from 'https';
import { Observable } from 'rxjs';
function verifyTurnstile(
secret: string,
token: string,
): Promise<{ success: boolean }> {
const body = new URLSearchParams({ secret, response: token }).toString();
return new Promise((resolve, reject) => {
const req = https.request(
{
hostname: 'challenges.cloudflare.com',
path: '/turnstile/v0/siteverify',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(body),
},
},
(res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (err) {
reject(err);
}
});
},
);
req.on('error', reject);
req.write(body);
req.end();
});
}
@Injectable()
export class TurnstileGuard implements CanActivate {
constructor(private configService: ConfigService) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
if (this.configService.get<string>('STATE') === 'desarrollo') return true;
const token = context.switchToHttp().getRequest().headers.turnstile;
if (!token)
throw new BadRequestException('No se envió un token de turnstile.');
const secret = this.configService.get<string>('TURNSTILE_SECRET_KEY');
return verifyTurnstile(secret, token)
.then((data) => {
if (data.success === true) return true;
throw new ForbiddenException('El token de turnstile no es válido.');
})
.catch((err) => {
if (
err instanceof ForbiddenException ||
err instanceof BadRequestException
)
throw err;
throw new InternalServerErrorException(
'Ocurrió un error con turnstile.',
);
});
}
}
+2 -2
View File
@@ -17,7 +17,7 @@ import {
ApiTags,
} from '@nestjs/swagger';
import { Serealize } from '../interceptors/serialize.interceptor';
import { HcaptchaGuard } from '../guards/hcaptcha.guard';
import { TurnstileGuard } from '../guards/turnstile.guard';
import { UsuarioService } from './usuario.service';
import { ValidarUsuarioService } from '../validar-usuario/validar-usuario.service';
import { Operador } from '../operador/entity/operador.entity';
@@ -39,7 +39,7 @@ export class UsuarioController {
@Serealize(MessageOutputDto)
@Post('registrar')
@UseGuards(HcaptchaGuard)
@UseGuards(TurnstileGuard)
@ApiOperation({ description: 'Registro de usuario.' })
@ApiBody({
description: 'Variables que necesita el endpoint.',