jwt-sidejacking-security

소개

JWT 는 stateless 성격을 가지고 있습니다. 그래서 별도의 세션 관리를 위한 DB가 필요없어 Microservice 환경에서도 많이 사용이 되고 있는데요.

하지만 이로인해 정해진 만료시간(expire) 시간 전에는 토큰이 유출되었다고 하더라도 유출된 토큰만 서버에서 파기할 방안이 없습니다. 그래서 짧은 만료시간을 설정하기를 권고하곤 하는데요.

공격자가 JWT 토큰을 가로채 정상 사용자로 가장하는 공격인 JWT sidejacking 공격에 취약할 수 있는데 이를 보완할 방안에 대해 한번 살펴보려 합니다.

JWT Sidejacking

JWT 는 어플리케이션이나 시스템의 인증 및 관련 정보 교환을 위해 사용되는 JSON Web Token 입니다. JWT 가 유출되거나 공격자가 이 토큰을 가로챈다면 정상 사용자로 인증을 할 수 있습니다.

JWT sidejacking 은 이처럼 공격자가 인증 token 을 가로채 정상 사용자로 가장하는 공격입니다.

JWT sidejacking 을 보안적으로 보완하기 위해서는 token 이 하이재킹 당하든 유출이 되든 token 만으로 인증을 할수 있도록 하기보다, 별도의 context 를 만들어 추가 검증을 할 수 있는 메커니즘을 만들어야 하는데요.

OWASP 에서는 별도의 user context 를 구현하여 JWT sidejacking 공격에 대한 보안적 방안을 제시해주고 있습니다.

JWT Sidejacking 보안 방안

user context 를 구현하라는 의미는 아래와 같습니다.

  • JWT 와 더불어 추가 검증을 위한 context 를 만듭니다.
  • context 라 함은 random string 을 만들어 Cookie 헤더에 저장(wiht SameSite, HttpOnly, Secure flags)하고,
  • 만든 random string 의 hash 값을 JWT 의 paylaod 에 추가합니다.
  • client 에서 server 로 응답이 오면 서버에서는 user context 를 검증합니다.
  • 검증은 아래 두값이 동일한지 확인을 하여 이루어집니다.
    • Cookie 에 저장된 random string 을 hashing
    • JWT payload 내 hashing 된 random string
  • 동일하다면 인증 성공

sequence diagram 으로 확인하면 아래와 같습니다.

jwt-security-diagram

해당 flow 를 통해 JWT 가 유출되어 사용되더라도 cookie(보안 flags 가 적용된)에 저장된 랜덤값과 다르다면 검증실패하여 인증이 되지 않을 것입니다. JWT 의 payload 값이 디코딩으로 확인이 되더라도 랜덤값의 hash 된 값이기 때문에 cookie 에 적용해야할 랜덤값 유추도 힘들어 집니다.

Node.js 로 구현

node express 를 이용하여 간단하게 구현해보면 아래와 같습니다.

import express from "express";
import crypto from "crypto";
import jwt from "jsonwebtoken";

const app = express();
const port = 3000;

app.get("/generate-token", (req, res) => {
  // Random data generator
  const secureRandom = crypto.randomBytes;

  // Generate a random string that will constitute the fingerprint for this user
  const randomFgp = secureRandom(50);
  const userFingerprint = randomFgp.toString("hex");

  // Add the fingerprint in a hardened cookie
  const fingerprintCookie = `__Secure-Fgp=${userFingerprint}; SameSite=Strict; HttpOnly; Secure`;
  res.setHeader("Set-Cookie", fingerprintCookie);

  // Compute a SHA256 hash of the fingerprint to store in the token
  const hash = crypto.createHash("sha256");
  hash.update(userFingerprint);
  const userFingerprintDigest = hash.digest("hex");

  // Create the token with a validity of 15 minutes and client context (fingerprint) information
  const now = Math.floor(Date.now() / 1000);
  const expirationTime = now + 15 * 60;
  const headerClaims = {
    typ: "JWT",
  };
  const secret = "secretKey"; // Use a secure key
  const token = jwt.sign(
    {
      sub: "user-login",
      exp: expirationTime,
      iss: "issuer-id",
      iat: now,
      nbf: now,
      userFingerprint: userFingerprintDigest,
    },
    secret,
    {
      header: headerClaims,
      algorithm: "HS256",
    }
  );

  // Send the token to the client
  res.send({ token });
});

app.listen(port, () => {
  console.log(`http://localhost:${port}`);
});

/generate-token 엔드포인트 접속하여 잘 동작하는지 JWT 와 Cookie 를 확인해봅니다.

jwt-node

jwt

Cookie 의 plain random string 을 hashing 하고,

JWT payload 의 hashing random string 과 동일한지 아래 코드로 검증해봅니다.

import crypto from "crypto";

// plain random string in cookie to hash
const userFingerprint = "8c9fc6bf6f039f96ae06289e4b6d76103ff9171eabd6a47606b58c0f5ce0ad9a9888594fd1658e5908bf29d1881c30881628";

const hash = crypto.createHash("sha256");
hash.update(userFingerprint);
const userFingerprintDigest = hash.digest("hex");

// hashing random string in JWT payload
const hasgedUserFingerprint =
  "801ac5401d3f5983f44f2a61265e1667d5faef8b6d0d9287ee63cdc8ef852125";

// verify
if (hasgedUserFingerprint === userFingerprintDigest) {
  console.log("pass");
} else {
  console.log("drop");
}

verify

잘 동작하네요.

Refs

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

Back To Top