Нещодавно я думав про те, як зробити деякі з моїх важливих кінцевих точок більш безпечними при використанні локально збереженого JWT.
Можливо, це не найкраща практика з точки зору безпеки (через можливість атак XSS), але це була вимога, яку я не встановлював. Довелося адаптуватися.
Тому, щоб зробити це більш безпечним, я знайшов рішення, яке, сподіваюся, допоможе і вам.
Я припускаю, що ми всі знаємо, що таке JWT — маркер, виданий серверною службою, який не може бути змінений зовнішнім інтерфейсом, оскільки це призведе до зміни підпису та автоматичного визнання маркера недійсним.
Звучить чудово, поки ми не зустрінемося з ситуацією, коли маркер може бути вкрадено - як у моєму випадку з XSS-атакою. Багато веб-сайтів зберігають маркери в локальному сховищі замість файлів cookie лише HTTP, тому вони також уразливі.
Отже, коли неможливо змінити спосіб, у який інтерфейс зберігає цей маркер, ми повинні змінити спосіб випуску та перевірки маркера, і це момент, коли я хочу представити вам просте рішення – JWT із хешованим відбитком, але давайте подивимось код
Припустімо, що у нас є кінцева точка для видачі токенів, назвемо її кінцевою точкою входу
@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, }; } }
Отже, те, що ми тут робимо, — це збирання агента користувача та віддаленої IP-адреси від користувача, який підключається, хешування, а потім поміщення в маркер JWT, тож навіть якщо користувач або зловмисник захоче побачити, що всередині маркера JWT, вони лише побачити випадкове хешоване значення
Але як це підвищить мою безпеку, запитаєте ви? Давайте подивимося на нашу другу кінцеву точку, покажемо відбиток пальця
@Controller('v1/fingerprint') @UseGuards(AuthGuard(JWT_STRATEGY)) export class ShowFingerPrintAction { @Get() async handle(@Req() request) { return { fingerprint: request.user.fingerprint, }; } }
Спочатку нічого особливого. Кінцева точка, яка повертає відбиток із запиту, тож що робить її безпечнішою? JWT_СТРАТЕГІЯ!
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; } }
Як бачите, у стратегії ми витягуємо ті самі значення, що й під час вилучення їх із кінцевої точки входу, і порівнюємо їх – якщо IP-адреса чи агент користувача змінилися, ми заборонимо доступ до нашої кінцевої точки відбитків пальців.
Це той випадок, коли навіть якщо зловмисник вкрав наш токен, він нічого не зможе з ним зробити, тому що
Ось як ми можемо захистити нашу важливу кінцеву точку дуже простим способом, але, звичайно, є певні компроміси
Для першої проблеми ви, звичайно, можете перевірити лише IP-адресу і навіть не хешувати її під час видачі маркера, але тоді потенційний зловмисник знає вектор атаки (підробка IP-адреси чи інші методи), тому безпечніше пожертвувати деякими продуктивність для безпеки.
Я сподіваюся, що цей метод допоможе вам запровадити додатковий рівень безпеки для ваших важливих кінцевих точок.
Посилання на робочий вихідний код: вихідний код
Поради: