Almon Dev

불충분한 세션 만료 보안 패치 본문

웹 해킹/웹 개발

불충분한 세션 만료 보안 패치

Almon 2025. 4. 14. 12:35

 1. 개요

 

불충분한 세션 만료 이란?

불충분한 세션 만료란 사용자의 인증 세션이 오랜 시간이 지나도 만료되지 않거나, 로그아웃 시 세션이 정상적으로 종료되지 않아 세션 ID가 유효하게 남아 있는 취약점을 의미합니다.

예를 들어, 사용자가 로그아웃하지 않고 브라우저를 종료하거나, 장시간 동안 아무런 활동 없이 세션이 유지되는 경우, 공격자가 해당 세션을 탈취하여 재사용할 수 있습니다. 특히 도서관, PC방 등 공용 컴퓨터에서 웹 서비스를 이용할 경우, 다음 사용자가 이전 사용자의 세션을 악용해 계정에 무단 접근하는 상황이 발생할 수 있습니다.

해당 취약점은 세션 탈취, 권한 악용, 민감 정보 탈취 등의 보안 사고가 발생할 수 있습니다.

 

보안 패치의 목적 및 중요성

공격자가 세션 식별자(세션 ID 등)를 탈취하는 등 사용자의 세션을 지속적으로 사용할 수 있는 경우 사용자 권한을 도용할 수 있습니다. 이를 통해 공격자는 계정에 접근하여 민감한 정보를 탈취하거나, 사용자 권한을 악용할 수 있습니다.

따라서 불충분한 세션 만료 취약점에 대한 보안 패치는 다음과 같은 목적을 가집니다.

  1. 사용자 활동이 일정 시간 이상 없을 경우 세션을 자동 만료시켜 비정상적인 세션 유지를 방지합니다.
  2. 로그아웃 시 서버에서 세션 정보를 삭제하여 세션 재사용을 차단합니다.
  3. 세션 인증에서 세션 만료 시 사용자에게 재인증을 요구해야 합니다.

 

 2. 취약점 분석

 

불충분한 세션 만료 발생 원인

JWT 만료 시간을 1일로 설정하여 유효 시간이 지나치게 긴 상태로 발급됩니다. 만약 세션이 탈취되면, 공격자가 장시간 동안 권한을 악용할 수 있습니다.

 

jwt_auth.php

....
    public static function createToken($id):bool|string {
        $header = self::url_safe_encode(json_encode(['arg' => self::$algorithm, 'typ' => 'JWT']));
        $payload = self::url_safe_encode(json_encode([
            "sub" => $id,
            "iat" => time(),
            "exp" => time() + (60 * 60 * 24), // JWT 만료 시간을 1일로 설정
        ]));
        $signature = self::url_safe_encode(hash_hmac(self::$algorithm, "$header.$payload", self::$secret_key, true)); 
        
        try {
            $token = "$header.$payload.$signature";
            setcookie("token", $token, time() + (60 * 60 * 24),"/");
        } catch (Exception $e) {
            return false;
        }
    
        return $token;
    }
....

 

불충분한 세션 만료 예시

JWT의 만료 시간이 지나치게 길게(1일) 설정되어 있으며, 클라이언트 인증 기반인 JWT의 특성상 로그아웃을 해도 서버에서 삭제할 수 없습니다. 이로 인해 JWT가 탈취되면 공격자가 장시간 동안 권한을 악용할 수 있습니다.

 

1. 로그인을 합니다.

 

2. 로그인 요청을 가로채 JWT의 만료 시간을 확인합니다.

 

3. 로그아웃을 합니다.

 

4. 로그아웃 시 JWT 쿠키가 클라이언트에서 삭제된 것을 확인합니다.

 

5. 삭제된 JWT를 복사합니다.

 

6. 삭제된 JWT값을 삽입한 token 쿠키를 생성합니다.

 

7. 쿠키를 생성한 뒤 새로고침을 하면 로그인 권한을 획득합니다.

 

 3. 보안 패치 적용

 

보안 조치 방법

JWT의 유효 기간을 짧게 설정하고, 리프레시 토큰을 사용해 토큰을 재발급받는 방식을 사용하면 세션 탈취로 인한 피해를 줄일 수 있습니다. 이 경우 리프레시 토큰이 탈취되지 않도록 안전하게 보호하는 것이 중요합니다.

 

PHP 예시)

require_once 'vendor/autoload.php';
use \Firebase\JWT\JWT;
....
// JWT 유효 기간 10분 설정
$tokenExp = 60 * 10;

// 리프레시 토큰 유효 기간 7일 설정
$refreshTokenExp = 60 * 60 * 24 * 7;

// JWT 생성
function createJwtToken($id) {
    global $secretKey, $tokenExp;
    $issuedAt = time();
    $exp = $issuedAt + $tokenExp; // JWT 만료 시간 설정

    $payload = array(
        "iat" => $issuedAt,
        "exp" => $exp,
        "id" => $id
    );

    return JWT::encode($payload, $secretKey);
}

// 리프레시 토큰 생성
function createRefreshToken($id) {
    global $refreshSecretKey, $refreshTokenExp;

    $issuedAt = time();
    $exp = $issuedAt + $refreshTokenExp; // 리프레시 토큰 만료 시간 설정

    $payload = array(
        "iat" => $issuedAt,
        "exp" => $exp,
        "id" => $id
    );

    return JWT::encode($payload, $refreshSecretKey);
}

// 리프레시 토큰을 이용해 JWT 재발급
function refreshJwt($refreshToken) {
    global $refreshSecretKey;

    try {
        $decoded = JWT::decode($refreshToken, $refreshSecretKey, array('HS256'));
        // 리프레시 토큰이 유효성 검사 후 JWT 생성
        return createJwt($decoded->id);
    } catch (Exception $e) {
        return false;
    }
}

 

수정 코드

JWT의 만료 시간을 10분으로 설정하고, 만료된 토큰에 대해서는 리프레시 토큰을 사용해 새로운 JWT를 발급하는 로직을 추가했습니다. 클라이언트는 만료된 JWT 대신 리프레시 토큰을 서버로 전송하고, 서버는 이를 검증하여 새로운 JWT를 발급합니다. 이 방식은 JWT가 탈취되더라도 유효 시간이 짧기 때문에 공격자가 악용할 수 있는 시간을 최소화할 수 있습니다. 또한, 리프레시 토큰은 HttpOnly 속성을 적용하여 JavaScript 접근을 차단하고, 이를 통해 XSS 공격 등으로 인한 탈취 위험을 감소시켰습니다.

 

jwt_auth.php -> createToken 함수 수정

    private static $secret_key = "DA5A431AD2AA76963B2DF723264CECCCDEF";
    private static $jwt_exp = time() + (60 * 60 * 10); // JWT 만료시간 10분으로 설정
    
    // JWT 생성
    public static function createToken($id):bool|string {
        $header = self::url_safe_encode(json_encode(['arg' => self::$algorithm, 'typ' => 'JWT']));
        $payload = self::url_safe_encode(json_encode([
            "sub" => $id,
            "iat" => time(),
            "exp" => time() + self::$jwt_exp,
        ]));
        $signature = self::url_safe_encode(hash_hmac(self::$algorithm, "$header.$payload", self::$secret_key, true)); 
        
        try {
            $token = "$header.$payload.$signature";
            setcookie(self::$cookie_name, $token, time() + self::$jwt_exp,"/");
            self::createRefreshToken($id);
        } catch (Exception $e) {
            return false;
        }
    
        return $token;
    }

 

jwt_auth.php -> createRefreshToken 함수 생성

    private static $ref_secret_key = "F23EA719C5B8DA64B3E2CD47A6F98012BC";
    private static $ref_exp = time() + (60 * 60 * 24 * 7); // 리프레시 토큰 만료시간 7일로 설정
    // 리프레시 토큰 생성
    public static function createRefreshToken($id):bool|string {
        $header = self::url_safe_encode(json_encode(['arg' => self::$algorithm, 'typ' => 'JWT']));
        $payload = self::url_safe_encode(json_encode([
            "sub" => $id,
            "iat" => time(),
            "exp" => time() + self::$ref_exp,
        ]));
        $signature = self::url_safe_encode(hash_hmac(self::$algorithm, "$header.$payload", self::$ref_secret_key, true)); 
        
        try {
            $ref_token = "$header.$payload.$signature";
            setcookie(
                self::$ref_cookie_name,
                $ref_token,
                [
                    'expires' => time() + self::$ref_exp,
                    'path' => '/',
                    'httponly' => true,
                ]
            );
        } catch (Exception $e) {
            return false;
        }
    
        return $ref_token;
    }

 

jwt_auth.php -> refToken 함수 생성

    public static function refToken($ref_token):bool|string {        
        try {
            // 디코딩과 서명 검증
            $token_split = explode('.', $ref_token);
            if (count($token_split) !== 3) {
                return false;
            }

            $header_b64 = $token_split[0];
            $payload_b64 = $token_split[1];
            $signature_b64 = $token_split[2];

            $header = json_decode(self::url_safe_decode($header_b64), true);
            $payload = json_decode(self::url_safe_decode($payload_b64), true);

            // 리프레시 토큰의 만료시간 검사
            if (time() > $payload['exp']) {
                return false;
            }

            $signature = self::url_safe_decode($signature_b64);
            $sig_test = hash_hmac(self::$algorithm, "$header_b64.$payload_b64", self::$ref_secret_key, true);

            // 리프레시 토큰 서명 검사
            if (hash_equals($signature, $sig_test)) {
                $token = self::createToken($payload['sub']);
                if ($token) {
                    // JWT 재발급 성공
                    return $token;
                }else {
                    return false;
                }
            }else {
                self::delRefToken();
                return false;
            }
        } catch (Exception $e) {
            // 에러
            return false;
        }
    }

 

jwt_auth.php -> delRefToken 함수 생성

    public static function delRefToken() {
        setcookie(self::$ref_cookie_name,"", time() - (60 * 60),"/");
    }

 

jwt_auth.php -> auth 함수 수정

    // JWT 검증 및 리프레시 토큰을 이용한 재발급
    public static function auth(): bool|stdClass
    {
        // JWT 확인
        if (isset($_COOKIE[self::$cookie_name])) {
            $token = $_COOKIE[self::$cookie_name];
            try {
                // 디코딩과 서명 검증
                $token_split = explode('.', $token);
                if (count($token_split) !== 3) {
                    return false;
                }

                $header_b64 = $token_split[0];
                $payload_b64 = $token_split[1];
                $signature_b64 = $token_split[2];

                $header = json_decode(self::url_safe_decode($header_b64), true);
                $payload = json_decode(self::url_safe_decode($payload_b64), true);
                // JWT 유효 시간 검사
                if (time() > $payload['exp']) {
                    if (isset($_COOKIE[self::$ref_cookie_name])) {
                        $ref_token_cookie = $_COOKIE[self::$ref_cookie_name];
                        $ref_token = self::refToken($ref_token_cookie);
                        if ($ref_token) {
                            $ref_token_split = explode('.', $ref_token);
                            $ref_payload_b64 = $ref_token_split[1];
                            $ref_payload = json_decode(self::url_safe_decode($ref_payload_b64), true);
                            return (object) $ref_payload;
                        } else {
                            return false;
                        }
                    } else {
                        return false;
                    }
                }

                $signature = self::url_safe_decode($signature_b64);
                $sig_test = hash_hmac(self::$algorithm, "$header_b64.$payload_b64", self::$secret_key, true);

                // JWT 서명 검사
                if (hash_equals($signature, $sig_test)) {
                    return (object) $payload;
                }else {
                    self::delToken();
                    return false;
                }
            } catch (Exception $e) {
                // 에러
                return false;
            }
        } else {
            // 리프레시 토큰 확인
            if (isset($_COOKIE[self::$ref_cookie_name])) {
                $ref_token_cookie = $_COOKIE[self::$ref_cookie_name];
                $ref_token = self::refToken($ref_token_cookie);
                if ($ref_token) {
                    $ref_token_split = explode('.', $ref_token);
                    $ref_payload_b64 = $ref_token_split[1];
                    $ref_payload = json_decode(self::url_safe_decode($ref_payload_b64), true);
                    return (object) $ref_payload;
                }
            }
            return false;
        }
    }

 

 4. 보안 패치 결과

 

보안 패치 후 테스트

테스트 결과, JWT가 만료되었을 때 리프레시 토큰이 존재하면 정상적으로 새로운 JWT가 발급되었으며, JWT 유효 시간이 10분으로 설정된 것을 확인했습니다. 이를 통해 세션 탈취로 인한 피해를 줄이면서도 사용자 경험을 유지할 수 있게 되었습니다.

 

1. 로그인을 합니다.

 

2. 서버의 응답에 포함된 JWT 쿠키의 유효 시간이 10분, 리프레시 토큰의 유효 시간이 7일로 설정되어 있는 것을 확인합니다.

 

3. JWT 쿠키가 만료되고, 리프레시 토큰이 있을 때 정상적으로 재발급이 되는 것을 확인합니다.

'웹 해킹 > 웹 개발' 카테고리의 다른 글

[Lord of SQL Injection] bugbear 문제 풀이  (1) 2025.04.22
CSRF 보안 패치  (1) 2025.04.11
약한 문자열 강도 보안 패치  (0) 2025.04.07
정보 누출 보안 패치  (0) 2025.04.05
디렉터리 인덱싱 보안 패치  (0) 2025.04.04