상위: Web Security
요약
Session은 서버가 클라이언트의 상태를 유지하기 위해 사용하는 전통적인 인증 방식입니다. 서버가 세션 데이터를 저장하고, 클라이언트는 세션 ID만 쿠키로 보관합니다.
세션 동작 원리
┌─────────┐ ┌─────────────┐
│ Client │ │ Server │
└────┬────┘ └──────┬──────┘
│ 1. 로그인 요청 │
│ (username, password) │
│────────────────────────────────────▶│
│ │
│ ┌────────────────────────────┐
│ │ 2. 세션 생성 │
│ │ session_id = "abc123" │
│ │ session_data = { │
│ │ user_id: 1, │
│ │ username: "john" │
│ │ } │
│ │ → 서버 메모리/DB에 저장 │
│ └────────────────────────────┘
│ │
│ 3. 세션 ID 쿠키 설정 │
│ Set-Cookie: session_id=abc123 │
│◀────────────────────────────────────│
│ │
│ 4. 이후 요청 │
│ Cookie: session_id=abc123 │
│────────────────────────────────────▶│
│ │
│ ┌────────────────────────────┐
│ │ 5. 세션 조회 │
│ │ sessions["abc123"] │
│ │ → user_id: 1 확인 │
│ └────────────────────────────┘
│ │
│ 6. 인증된 응답 │
│◀────────────────────────────────────│
세션 저장 방식
1. 메모리 (기본)
# Python Flask
from flask import Flask, session
app.secret_key = 'secret-key'
session['user_id'] = 123
장점: 빠름 단점: 서버 재시작 시 초기화, 확장성 제한
2. 파일 시스템
// PHP
session_save_path('/var/lib/php/sessions');
session_start();
$_SESSION['user_id'] = 123;
3. 데이터베이스
CREATE TABLE sessions (
session_id VARCHAR(128) PRIMARY KEY,
user_id INT,
data TEXT,
expires_at TIMESTAMP
);
4. Redis (권장)
# Flask-Session + Redis
from flask_session import Session
from redis import Redis
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = Redis(host='localhost', port=6379)
Session(app)
장점:
- 여러 서버 간 세션 공유
- 빠른 읽기/쓰기
- TTL(만료) 자동 관리
쿠키 보안 설정
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600
| 속성 | 설명 | 보안 효과 |
|---|---|---|
| HttpOnly | JavaScript 접근 차단 | XSS 방어 |
| Secure | HTTPS에서만 전송 | 도청 방지 |
| SameSite=Strict | 같은 사이트에서만 전송 | CSRF 방어 |
| SameSite=Lax | GET 요청은 허용 | 부분 CSRF 방어 |
| Path | 쿠키 전송 경로 제한 | 범위 제한 |
| Max-Age/Expires | 만료 시간 | 세션 수명 제한 |
세션 보안 취약점
1. Session Fixation
공격자가 심어둔 세션 ID를 피해자가 쓰게 유도 → 로그인 시 세션 ID 재생성으로 방어. 상세는 Session Fixation.
2. Session Hijacking
네트워크 스니핑·XSS로 세션 ID를 탈취 → HTTPS·HttpOnly·IP 검증으로 방어. 상세는 Session Hijacking.
3. Session Timeout
오래된 세션이 계속 유효
→ 공유 PC에서 로그아웃 안 하면 다음 사용자가 접근
방어:
# 세션 타임아웃 설정
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(minutes=30)
# 마지막 활동 시간 추적
@app.before_request
def update_last_activity():
session['last_activity'] = datetime.utcnow()
@app.before_request
def check_session_timeout():
if 'last_activity' in session:
inactive = datetime.utcnow() - session['last_activity']
if inactive > timedelta(minutes=30):
session.clear()
return redirect('/login')
4. Insufficient Session ID Randomness
# 취약: 예측 가능한 세션 ID
session_id = f"user_{user_id}_{timestamp}"
# 안전: 암호학적으로 안전한 랜덤
import secrets
session_id = secrets.token_urlsafe(32)
구현 예시
Python Flask
from flask import Flask, session, redirect, request
import secrets
app = Flask(__name__)
app.secret_key = secrets.token_hex(32)
# 세션 설정
app.config.update(
SESSION_COOKIE_SECURE=True, # HTTPS only
SESSION_COOKIE_HTTPONLY=True, # No JS access
SESSION_COOKIE_SAMESITE='Strict',
PERMANENT_SESSION_LIFETIME=timedelta(hours=1)
)
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
user = authenticate(username, password)
if user:
session.clear() # 세션 고정 방지
session['user_id'] = user.id
session['ip'] = request.remote_addr
session.permanent = True
return redirect('/dashboard')
return 'Login failed', 401
@app.route('/logout')
def logout():
session.clear()
return redirect('/login')
@app.route('/dashboard')
def dashboard():
if 'user_id' not in session:
return redirect('/login')
return f"Welcome, User {session['user_id']}"
Express.js
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const redis = require('redis');
const redisClient = redis.createClient();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // No JS access
sameSite: 'strict',
maxAge: 3600000 // 1 hour
}
}));
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (authenticate(username, password)) {
// 세션 재생성 (fixation 방지)
req.session.regenerate((err) => {
req.session.userId = user.id;
res.redirect('/dashboard');
});
} else {
res.status(401).send('Login failed');
}
});
app.get('/logout', (req, res) => {
req.session.destroy();
res.redirect('/login');
});
Session vs JWT 선택
| 시나리오 | 권장 |
|---|---|
| 전통적 웹 앱 (서버 렌더링) | Session |
| 단일 서버 | Session |
| 여러 서버 + 공유 저장소 | Session (Redis) |
| Stateless API | JWT |
| 모바일 앱 | JWT |
| 마이크로서비스 | JWT |
| 즉시 무효화 필요 | Session |
보안 체크리스트
- 세션 ID는 암호학적으로 안전한 랜덤 값
- HttpOnly, Secure, SameSite 쿠키 속성 설정
- 로그인 시 세션 ID 재생성
- 적절한 세션 타임아웃 설정
- 로그아웃 시 서버 측 세션 완전 삭제
- 민감 작업 시 재인증 요구
- 동시 세션 제한 (선택)
- IP/User-Agent 바인딩 (선택)
관련 공격
- Session Hijacking - 세션 탈취
- Session Fixation - 세션 고정
- CSRF - 세션 쿠키 악용
- XSS - 세션 ID 탈취