FlawOpen

flawopen.com/Vulnerabilities/세션 고정 및 쿠키 보안

세션 고정: 로그인 시 ID 교체 (CWE-384)

High Severity CWE-384 Cryptography & Auth
💡 쉽게 설명 — 붙어 버린 수하물표

공격자가 이미 알고 있는 수하물표 번호를 여행자가 쓰도록 속였다고 상상해 보세요. 항공사가 수하물을 맡긴 뒤에도 같은 번호를 유지하면 공격자가 가방을 찾아갈 수 있습니다. 세션 고정은 애플리케이션이 공격자가 피해자에게 사용하게 만들 수 있는 식별자를 받아들인 뒤, 그 식별자를 바꾸지 않고 인증된 세션으로 승격하는 문제입니다. 두 조건이 모두 필요합니다. 애플리케이션이 해당 식별자를 받아들이거나 설정할 수 있게 하지 않는다면 일반적인 피싱 링크만으로 세션을 고정할 수 없습니다.

대상: 서버 측 세션 관리와 인증된 브라우저가 사용하는 식별자.
전제 조건: 공격자가 허용된 URL 기반 ID나 쿠키 주입 등을 통해 피해자가 아는 세션 ID를 사용하게 만들 수 있어야 합니다.
결함: 애플리케이션이 익명 세션에서 인증 세션으로 전환할 때 공격자가 아는 ID를 유지합니다.
대응: 인증 시 예측하기 어려운 새 ID를 발급하고, 서로 다른 위협에 맞춰 쿠키 속성을 설정합니다.

핵심 개념과 하위 시스템 용어

세션 고정
애플리케이션이 공격자가 아는 식별자를 받아들이고 피해자가 인증한 뒤에도 유지하는 세션 관리 결함입니다.
세션 ID 교체
인증으로 세션 권한이 상승할 때 새롭고 예측하기 어려운 식별자를 발급합니다. 애플리케이션이 유지해야 하는 세션 데이터만 보존합니다.
쿠키 속성
Secure는 HTTPS 전송만 허용하고, HttpOnly는 JavaScript의 쿠키 읽기를 막으며, SameSite는 일부 교차 사이트 전송을 제어합니다. 각각 다른 위험에 대응하므로 ID 교체를 대신할 수 없습니다.
__Host- 접두사
지원 브라우저에서는 HTTPS를 통해 설정한 Secure 쿠키, Path=/, Domain 속성 없음이 요구됩니다. 인접 하위 도메인이 같은 이름으로 상위 도메인 쿠키를 설정하기 어렵게 합니다.

단계별 공격 흐름

Step 1

공격자가 세션 식별자를 얻거나 선택

별도의 취약점이나 기능으로 공격자가 ID를 얻고 피해자 브라우저가 이를 보내게 합니다. 방법은 애플리케이션마다 다릅니다. URL 세션 ID를 허용하지 않는다면 일반적인 피싱 링크만으로는 이 단계를 만들 수 없습니다.

Step 2

피해자가 공격자가 아는 식별자를 사용

브라우저는 해당 ID를 애플리케이션에 보내고, 애플리케이션은 유효한 익명 세션으로 처리합니다. 공격자는 같은 식별자를 알아보거나 다시 사용할 수 있어야 합니다.

Step 3

인증으로 세션 권한이 상승

피해자가 로그인합니다. 애플리케이션이 기존 ID를 교체하지 않고 인증된 신원을 연결하면 공격자가 아는 식별자가 인증된 세션을 가리킵니다.

Step 4

공격자가 바뀌지 않은 식별자를 재사용

공격자가 여전히 ID를 보낼 수 있고 서버가 이를 무효화하지 않았다면 피해자 세션에 접근할 수 있습니다. 권한 변경 시 ID를 교체하면 이 공격 경로를 끊을 수 있습니다.

메커니즘과 근본 원인

세션 고정에는 공격자가 피해자가 사용할 식별자에 영향을 미칠 수 있고, 로그인 성공 후 애플리케이션이 그 식별자를 교체하지 않는다는 두 조건이 필요합니다. 쿠키 속성은 심층 방어 수단이지만 ID 교체를 대신하지 않습니다. HttpOnly는 스크립트 읽기를 제한하고, Secure는 HTTPS를 요구하며, SameSite는 일부 교차 사이트 전송을 줄이고, __Host-는 지원 브라우저에서 쿠키 범위를 제한합니다.

login_view.py(취약한 패턴)취약함
# VULNERABLE: custom session code keeps the pre-authentication ID.
def login_view(request):
    user = authenticate(request, request.POST["username"],
                        request.POST["password"])
    if user is not None:
        # BUG: privilege changes, but the session ID does not.
        request.session["user_id"] = user.pk
        request.session["authenticated"] = True
        return redirect("/account/")
    return redirect("/login/")
login_view.py(Django 안전 예시)보호됨
# HARDENED: Django login() rotates the session key on authentication.
from django.contrib.auth import authenticate, login

def login_view(request):
    user = authenticate(request, request.POST["username"],
                        request.POST["password"])
    if user is not None:
        # Uses Django's supported authentication/session integration.
        login(request, user)
        return redirect("/account/")
    return redirect("/login/")

# settings.py: use HTTPS, host-only scope, and a deliberate SameSite policy.
SESSION_COOKIE_NAME = "__Host-sessionid"
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
SESSION_COOKIE_PATH = "/"
SESSION_COOKIE_DOMAIN = None

공격 순서와 개념 예시

엔지니어링 방어 및 예방 규칙

출처 및 관련 보안 주제:전체 보안 디렉터리 →
CWE-384: 세션 고정 → OWASP: 세션 관리 치트 시트 → OWASP WSTG: 세션 고정 테스트 → Django: 세션과 cycle_key() → MDN: HTTP 쿠키와 접두사 →