paint-brush
Si no puedes controlar el almacenamiento, controla el accesopor@axotion
199 lecturas

Si no puedes controlar el almacenamiento, controla el acceso

por Kamil Fronczak4m2025/01/18
Read on Terminal Reader

Demasiado Largo; Para Leer

Últimamente, he estado pensando en cómo hacer que algunos de mis puntos finales cruciales sean más seguros al utilizar JWT almacenado localmente.
featured image - Si no puedes controlar el almacenamiento, controla el acceso
Kamil Fronczak HackerNoon profile picture

Últimamente, he estado pensando en cómo hacer que algunos de mis puntos finales cruciales sean más seguros al utilizar JWT almacenado localmente.


Puede que no sea la mejor práctica para la seguridad (debido a la posibilidad de ataques XSS), pero no era un requisito que yo estableciera. Tuve que adaptarme.


Para que esto sea más seguro, encontré una solución que espero que también te ayude.

El problema

Supongo que todos sabemos qué es JWT: un token emitido por el servicio backend, que no puede ser modificado por el frontend, porque eso cambiaría la firma e invalidaría automáticamente el token.


Suena genial, hasta que nos encontramos con una situación en la que el token puede ser robado, como en mi caso con el ataque XSS. Muchos sitios web almacenan tokens en el almacenamiento local en lugar de en cookies exclusivas de HTTP, por lo que también son vulnerables.

La solución

Entonces, cuando no es posible cambiar la forma en que el frontend almacena ese token, tenemos que cambiar la forma en que emitimos y validamos el token, y este es el momento en el que quiero presentarles una solución simple: JWT con huella digital hash, pero veamos el código.


Supongamos que tenemos un punto final para emitir tokens, llamémoslo punto final de inicio de sesión.

 @Controller('v1/sign-in') export class SignInAction { constructor(private jwtService: JwtService) {} @Post() async handle( @Req() request: Request, @Body() body: SignInHttpRequest, ): Promise<SignInHttpResponse> { const ip = request.headers['x-forwarded-for'] || request.socket.remoteAddress; const userAgent = request.headers['user-agent']; const token = this.jwtService.sign({ email: body.email, fingerprint: getSHA512Hash(`${ip}${userAgent}`), // It's worth to mention, that to make it even more secure, you should add salt }); return { token, }; } }


Entonces, lo que estamos haciendo aquí es básicamente recolectar el agente de usuario y la IP remota del usuario que se está conectando, codificarlo y luego colocarlo en el token JWT, por lo que incluso si el usuario o el atacante quisieran ver qué hay dentro del token JWT, solo verán un valor codificado aleatorio.


Pero, ¿cómo va a mejorar mi seguridad? Echemos un vistazo a nuestro segundo punto final, mostremos la huella digital.


 @Controller('v1/fingerprint') @UseGuards(AuthGuard(JWT_STRATEGY)) export class ShowFingerPrintAction { @Get() async handle(@Req() request) { return { fingerprint: request.user.fingerprint, }; } }


En un principio, no es nada especial. Punto final que devuelve la huella digital de la solicitud, ¿qué lo hace más seguro? ¡JWT_STRATEGY!


 export const JWT_STRATEGY = 'JWT'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy, JWT_STRATEGY) { constructor() { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: 'hard!to-guess_secret', passReqToCallback: true, }); } async validate(request: Request, payload: any): Promise<any> { const fingerprint = payload.fingerprint; const ip = request.headers['x-forwarded-for'] || request.socket.remoteAddress; const userAgent = request.headers['user-agent']; const calculatedFingerprint = getSHA512Hash(`${ip}${userAgent}`); // It's worth to mention, that to make it even more secure, you should add salt if (fingerprint !== calculatedFingerprint) { throw new BadRequestException('Invalid fingerprint'); } return payload; } }

Como puede ver, en la estrategia, extraemos los mismos valores que cuando los extrajimos del punto final de inicio de sesión y los comparamos: si la IP o el agente de usuario han cambiado, denegaremos el acceso a nuestro punto final de huella digital.


Este es el caso en el que incluso si el atacante robara nuestro token, no podría hacer nada con él, porque

  • Él no sabe qué es el hash dentro de la huella digital.
  • No le permitiremos conectarse a nuestros puntos finales debido a una IP y un agente de usuario diferentes (y quizás más factores)


Así es como podemos proteger nuestro punto final crucial de una manera muy sencilla, aunque, por supuesto, hay algunas desventajas.

  • Para cada llamada, tenemos que calcular el hash sha512
  • Si el usuario tiene una IP dinámica tendrá que iniciar sesión con frecuencia


Para el primer problema, por supuesto, podrías verificar solo la IP y ni siquiera hacer un hash al emitir un token, pero entonces el atacante potencial conoce el vector del ataque (suplantación de IP u otros métodos), por lo que es más seguro sacrificar algo de rendimiento por seguridad.


Espero que este método le ayude a introducir una capa adicional de seguridad en sus puntos finales cruciales.


Enlace al código fuente funcional: Código fuente


Consejos:

  • El encabezado X-FORWARDER-FOR se puede falsificar fácilmente a menos que uses servicios como Cloudflare, tenlo en cuenta