最近、ローカルに保存された JWT を使用する際に、いくつかの重要なエンドポイントをより安全にする方法について考えていました。
これはセキュリティ上のベストプラクティスではないかもしれませんが (XSS 攻撃の可能性があるため)、これは私が設定した要件ではありません。適応する必要がありました。
そこで、これをより安全にするために、あなたにも役立つであろう解決策を見つけました。
JWT が何であるかは、皆さんご存知だと思います。JWT は、バックエンド サービスによって発行されるトークンであり、フロントエンドでは変更できません。変更すると、署名が変更され、トークンが自動的に無効になるためです。
トークンが盗まれる可能性がある状況に遭遇するまでは、素晴らしいように思えます。私のケースでは、XSS 攻撃がありました。多くの Web サイトは、HTTP のみの Cookie ではなく、ローカル ストレージにトークンを保存しているため、同様に脆弱です。
フロントエンドがトークンを保存する方法を変更できない場合は、トークンを発行して検証する方法を変更する必要があります。ここで、ハッシュ化されたフィンガープリントを使用した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トークンの中身を見たいと思っても、ランダムなハッシュ値しか見えません。
しかし、それでどのようにセキュリティが強化されるのでしょうか?2番目のエンドポイントを見てみましょう。指紋を表示します
@Controller('v1/fingerprint') @UseGuards(AuthGuard(JWT_STRATEGY)) export class ShowFingerPrintAction { @Get() async handle(@Req() request) { return { fingerprint: request.user.fingerprint, }; } }
最初は、特別なことではありません。リクエストからフィンガープリントを返すエンドポイントですが、何がより安全になるのでしょうか? 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; } }
ご覧のとおり、戦略では、サインイン エンドポイントから抽出したときと同じ値を抽出して比較しています。IP またはユーザー エージェントが変更されている場合は、指紋エンドポイントへのアクセスを拒否します。
これは、たとえ攻撃者がトークンを盗んだとしても、それを使って何もできないケースです。
このように非常に簡単な方法で重要なエンドポイントを保護することができますが、もちろんいくつかのトレードオフがあります。
最初の問題については、もちろん IP のみをチェックし、トークンを発行するときにハッシュ化も行わないこともできますが、その場合、潜在的な攻撃者は攻撃のベクトル (IP スプーフィングまたはその他の方法) を知っているため、セキュリティのためにパフォーマンスをある程度犠牲にする方が安全です。
この方法が、重要なエンドポイントに追加のセキュリティ層を導入するのに役立つことを願っています。
動作するソースコードへのリンク:ソースコード
ヒント: