paint-brush
저장 공간을 제어할 수 없다면 액세스를 제어하세요~에 의해@axotion
199 판독값

저장 공간을 제어할 수 없다면 액세스를 제어하세요

~에 의해 Kamil Fronczak4m2025/01/18
Read on Terminal Reader

너무 오래; 읽다

최근 저는 로컬에 저장된 JWT를 사용하여 일부 중요한 엔드포인트의 보안을 강화하는 방법에 대해 생각해 왔습니다.
featured image - 저장 공간을 제어할 수 없다면 액세스를 제어하세요
Kamil Fronczak HackerNoon profile picture

최근 저는 로컬에 저장된 JWT를 사용하여 일부 중요한 엔드포인트의 보안을 강화하는 방법에 대해 생각해 왔습니다.


보안을 위한 최선의 관행은 아닐 수 있지만(XSS 공격 가능성 때문에), 이는 내가 정한 요구 사항이 아니었습니다. 나는 적응해야 했습니다.


그래서 이를 더 안전하게 만들기 위해 저는 해결책을 찾았는데, 이것이 여러분에게도 도움이 되기를 바랍니다.

문제

JWT가 무엇인지 우리 모두 알고 있다고 가정하겠습니다. JWT는 백엔드 서비스에서 발행한 토큰으로, 프런트엔드에서 수정할 수 없습니다. 프런트엔드에서 수정하면 서명이 변경되어 토큰이 자동으로 무효화되기 때문입니다.


토큰이 도난당할 수 있는 상황(내 경우 XSS 공격)에 직면할 때까지는 좋은 것처럼 들립니다. 많은 웹사이트가 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_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 및 사용자 에이전트(그리고 아마도 다른 요인들)로 인해 그가 우리 엔드포인트에 연결하는 것을 허용하지 않을 것입니다.


이것이 우리가 매우 간단한 방법으로 중요한 엔드포인트를 보호할 수 있는 방법이지만 물론 몇 가지 상충도 있습니다.

  • 모든 호출에 대해 sha512 해시를 계산해야 합니다.
  • 사용자가 동적 IP를 사용하는 경우 자주 로그인해야 합니다.


첫 번째 문제의 경우, 토큰을 발급할 때 IP만 확인하고 해싱하지 않을 수도 있습니다. 하지만 그러면 잠재적 공격자는 공격 벡터(IP 스푸핑 또는 기타 방법)를 알게 되므로 보안을 위해 성능을 일부 희생하는 것이 더 안전합니다.


이 방법이 귀하의 중요한 엔드포인트에 추가적인 보안 계층을 도입하는 데 도움이 되기를 바랍니다.


작동 소스 코드에 대한 링크: 소스 코드


팁:

  • Cloudflare와 같은 서비스를 사용하지 않는 한 X-FORWARDER-FOR 헤더는 쉽게 스푸핑될 수 있으므로 이 점을 명심하세요.