Nedavno sam razmišljao o tome kako neke od mojih ključnih krajnjih tačaka učiniti sigurnijim kada koristim lokalno pohranjeni JWT.
Možda to nije najbolja praksa za sigurnost (zbog mogućnosti XSS napada), ali to je bio zahtjev koji nisam postavio. Morao sam se prilagoditi.
Dakle, kako bih ovo učinio sigurnijim, pronašao sam rješenje koje će, nadam se, pomoći i vama.
Pretpostavljam da svi znamo šta je JWT - token koji izdaje backend servis, a koji frontend ne može modificirati, jer bi to promijenilo potpis i automatski poništilo token.
Zvuči odlično, dok se ne nađemo u situaciji da token može biti ukraden - kao u mom slučaju sa XSS napadom. Mnoge web stranice pohranjuju tokene u lokalnu pohranu umjesto kolačića samo za HTTP, tako da su i oni ranjivi.
Dakle, kada nije moguće promijeniti način na koji frontend pohranjuje taj token, moramo promijeniti način na koji izdajemo i validiramo token, a ovo je trenutak kada želim da vas upoznam sa jednostavnim rješenjem - JWT sa heširanim otiskom prsta, ali da vidimo kod
Pretpostavimo, da imamo krajnju tačku za izdavanje tokena, nazovimo je krajnja tačka za prijavu
@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, }; } }
Dakle, ono što mi radimo ovdje je u osnovi prikupljanje korisničkog agenta i udaljene IP adrese od korisnika koji se povezuje, heširanje i zatim stavljanje u JWT token, pa čak i ako korisnik ili napadač želi vidjeti šta je unutar JWT tokena, oni će samo pogledajte neku nasumično heširanu vrijednost
Ali kako će to povećati moju sigurnost pitate se? Hajde da pogledamo našu drugu krajnju tačku, pokažimo otisak prsta
@Controller('v1/fingerprint') @UseGuards(AuthGuard(JWT_STRATEGY)) export class ShowFingerPrintAction { @Get() async handle(@Req() request) { return { fingerprint: request.user.fingerprint, }; } }
U početku, nije ništa posebno. Krajnja tačka koja vraća otisak prsta iz zahtjeva, pa šta je čini sigurnijom? 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; } }
Kao što možete vidjeti, u strategiji izdvajamo iste vrijednosti kao kada smo ih izdvojili iz krajnje tačke za prijavu i upoređujemo ih - ako se IP ili korisnički agent promijenio, tada ćemo zabraniti pristup našoj krajnjoj tački otiska prsta.
Ovo je slučaj kada čak i ako nam je napadač ukrao token, ne može ništa s njim, jer
Tako možemo zaštititi našu ključnu krajnju tačku na vrlo jednostavan način, ali naravno, postoje neki kompromisi
Za prvi problem, naravno, možete provjeriti samo IP i čak ga ne heširati prilikom izdavanja tokena, ali tada potencijalni napadač zna vektor napada (IP lažiranje ili druge metode), pa je sigurnije žrtvovati neke performanse za sigurnost.
Nadam se da će vam ova metoda pomoći da uvedete dodatni sloj sigurnosti na vaše ključne krajnje tačke.
Link do radnog izvornog koda: Izvorni kod
Savjeti: