세션 관리 보안
개요
세션 관리는 사용자 인증 상태를 유지하고 보안을 보장하는 핵심 요소입니다. 특히 User Token Delegation Pattern에서는 중간 서비스가 사용자 세션을 안전하게 관리해야 하므로, 강화된 보안 조치가 필요합니다.
현재 구현 상태
dta-wide-mcp:
- 기본 세션 관리 구현 (메모리 기반)
- 세션 만료 정책 (24시간 절대 만료, 30분 비활성 만료)
- 기본적인 세션 정리 메커니즘
dta-wide-api:
- JWT 기반 세션 관리
- 리프레시 토큰을 통한 세션 연장
- 기본적인 세션 보안 헤더 적용
향후 계획:
- 고급 세션 보안 기능 (CSRF 보호, 세션 고정 방지)
- 분산 세션 저장소 (Redis)
- 실시간 세션 모니터링 및 이상 탐지
세션 보안 위험
1. 세션 하이재킹 (Session Hijacking)
위험 시나리오:
- 네트워크 스니핑을 통한 세션 ID 탈취
- XSS 공격을 통한 세션 쿠키 획득
- 세션 고정 공격 (Session Fixation)
대응 방안:
class SecureSessionManager {
private readonly sessions = new Map<string, SessionData>();
async createSecureSession(userId: string, metadata: SessionMetadata): Promise<string> {
// 기존 세션 무효화 (세션 고정 공격 방지)
await this.invalidateUserSessions(userId);
// 암호학적으로 안전한 세션 ID 생성
const sessionId = this.generateSecureSessionId();
// 세션 데이터 생성
const sessionData: SessionData = {
userId,
createdAt: Date.now(),
lastAccessAt: Date.now(),
ipAddress: metadata.ipAddress,
userAgent: metadata.userAgent,
deviceFingerprint: await this.generateDeviceFingerprint(metadata),
isSecure: true,
csrfToken: this.generateCSRFToken()
};
this.sessions.set(sessionId, sessionData);
// 세션 생성 로깅
this.auditLogger.log('SESSION_CREATED', {
sessionId: this.hashSessionId(sessionId),
userId,
ipAddress: metadata.ipAddress,
userAgent: this.sanitizeUserAgent(metadata.userAgent)
});
return sessionId;
}
private generateSecureSessionId(): string {
// 256비트 엔트로피 사용
const randomBytes = crypto.randomBytes(32);
return randomBytes.toString('base64url');
}
private async generateDeviceFingerprint(metadata: SessionMetadata): Promise<string> {
const fingerprintData = {
userAgent: metadata.userAgent,
acceptLanguage: metadata.acceptLanguage,
timezone: metadata.timezone,
screenResolution: metadata.screenResolution
};
const hash = crypto.createHash('sha256');
hash.update(JSON.stringify(fingerprintData));
return hash.digest('hex');
}
}
2. 세션 고정 공격 (Session Fixation)
위험 시나리오:
- 공격자가 미리 생성한 세션 ID를 피해자에게 강제 사용
- 피해자 로그인 후 공격자가 동일 세션으로 접근
대응 방안:
class SessionFixationPrevention {
async regenerateSessionOnLogin(oldSessionId: string, userId: string): Promise<string> {
// 기존 세션 데이터 백업
const oldSession = this.sessions.get(oldSessionId);
if (oldSession) {
// 기존 세션 무효화
this.sessions.delete(oldSessionId);
// 새로운 세션 ID 생성
const newSessionId = this.generateSecureSessionId();
// 세션 데이터 이전 (사용자 정보 업데이트)
const newSession: SessionData = {
...oldSession,
userId, // 인증된 사용자 ID로 업데이트
sessionId: newSessionId,
regeneratedAt: Date.now(),
loginTime: Date.now()
};
this.sessions.set(newSessionId, newSession);
// 세션 재생성 로깅
this.auditLogger.log('SESSION_REGENERATED', {
oldSessionId: this.hashSessionId(oldSessionId),
newSessionId: this.hashSessionId(newSessionId),
userId
});
return newSessionId;
}
throw new Error('Invalid session for regeneration');
}
}
3. 동시 세션 관리
보안 정책:
- 사용자당 최대 동시 세션 수 제한
- 새 로그인시 기존 세션 처리 방식 정의
- 의심스러운 동시 접근 감지
구현:
class ConcurrentSessionManager {
private readonly maxSessionsPerUser = 3;
private readonly userSessions = new Map<string, Set<string>>();
async manageConcurrentSessions(userId: string, newSessionId: string): Promise<void> {
const userSessionSet = this.userSessions.get(userId) || new Set();
// 최대 세션 수 초과시 가장 오래된 세션 제거
if (userSessionSet.size >= this.maxSessionsPerUser) {
const oldestSession = await this.findOldestSession(userId);
if (oldestSession) {
await this.terminateSession(oldestSession, 'MAX_SESSIONS_EXCEEDED');
userSessionSet.delete(oldestSession);
}
}
// 새 세션 추가
userSessionSet.add(newSessionId);
this.userSessions.set(userId, userSessionSet);
// 동시 세션 모니터링
await this.monitorConcurrentSessions(userId);
}
private async monitorConcurrentSessions(userId: string): Promise<void> {
const sessions = this.userSessions.get(userId) || new Set();
const sessionDetails = await Promise.all(
Array.from(sessions).map(sessionId => this.getSessionDetails(sessionId))
);
// 서로 다른 지역에서의 동시 접근 감지
const uniqueLocations = new Set(
sessionDetails.map(session => session.location).filter(Boolean)
);
if (uniqueLocations.size > 1) {
this.securityAlerts.emit('SUSPICIOUS_CONCURRENT_ACCESS', {
userId,
locations: Array.from(uniqueLocations),
sessionCount: sessions.size
});
}
}
}
세션 생명주기 관리
1. 세션 만료 정책
interface SessionExpirationPolicy {
absoluteTimeout: number; // 절대 만료 시간
idleTimeout: number; // 비활성 만료 시간
renewalThreshold: number; // 갱신 임계값
maxRenewals: number; // 최대 갱신 횟수
}
class SessionExpirationManager {
private readonly policies: Record<string, SessionExpirationPolicy> = {
standard: {
absoluteTimeout: 24 * 60 * 60 * 1000, // 24시간
idleTimeout: 30 * 60 * 1000, // 30분
renewalThreshold: 5 * 60 * 1000, // 5분
maxRenewals: 10
},
sensitive: {
absoluteTimeout: 8 * 60 * 60 * 1000, // 8시간
idleTimeout: 15 * 60 * 1000, // 15분
renewalThreshold: 2 * 60 * 1000, // 2분
maxRenewals: 5
}
};
async checkSessionExpiration(sessionId: string): Promise<SessionStatus> {
const session = this.sessions.get(sessionId);
if (!session) {
return { valid: false, reason: 'SESSION_NOT_FOUND' };
}
const now = Date.now();
const policy = this.policies[session.securityLevel] || this.policies.standard;
// 절대 만료 시간 확인
if (now - session.createdAt > policy.absoluteTimeout) {
await this.terminateSession(sessionId, 'ABSOLUTE_TIMEOUT');
return { valid: false, reason: 'ABSOLUTE_TIMEOUT' };
}
// 비활성 만료 시간 확인
if (now - session.lastAccessAt > policy.idleTimeout) {
await this.terminateSession(sessionId, 'IDLE_TIMEOUT');
return { valid: false, reason: 'IDLE_TIMEOUT' };
}
// 갱신 필요 여부 확인
const timeUntilExpiry = policy.idleTimeout - (now - session.lastAccessAt);
if (timeUntilExpiry < policy.renewalThreshold) {
if (session.renewalCount < policy.maxRenewals) {
await this.renewSession(sessionId);
return { valid: true, renewed: true };
} else {
await this.terminateSession(sessionId, 'MAX_RENEWALS_EXCEEDED');
return { valid: false, reason: 'MAX_RENEWALS_EXCEEDED' };
}
}
return { valid: true };
}
private async renewSession(sessionId: string): Promise<void> {
const session = this.sessions.get(sessionId);
if (session) {
session.lastAccessAt = Date.now();
session.renewalCount = (session.renewalCount || 0) + 1;
this.auditLogger.log('SESSION_RENEWED', {
sessionId: this.hashSessionId(sessionId),
userId: session.userId,
renewalCount: session.renewalCount
});
}
}
}
2. 세션 정리 및 가비지 컬렉션
class SessionCleanupManager {
private cleanupInterval: NodeJS.Timeout;
startCleanupScheduler(): void {
// 5분마다 만료된 세션 정리
this.cleanupInterval = setInterval(async () => {
await this.cleanupExpiredSessions();
}, 5 * 60 * 1000);
}
async cleanupExpiredSessions(): Promise<void> {
const now = Date.now();
const expiredSessions: string[] = [];
for (const [sessionId, session] of this.sessions.entries()) {
if (this.isSessionExpired(session, now)) {
expiredSessions.push(sessionId);
}
}
// 배치로 세션 정리
await Promise.all(
expiredSessions.map(sessionId =>
this.terminateSession(sessionId, 'CLEANUP_EXPIRED')
)
);
if (expiredSessions.length > 0) {
this.logger.log(`Cleaned up ${expiredSessions.length} expired sessions`);
}
}
private isSessionExpired(session: SessionData, now: number): boolean {
const maxAge = 24 * 60 * 60 * 1000; // 24시간
const maxIdle = 30 * 60 * 1000; // 30분
return (
now - session.createdAt > maxAge ||
now - session.lastAccessAt > maxIdle
);
}
async gracefulShutdown(): Promise<void> {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
}
// 모든 활성 세션에 종료 알림
const activeSessions = Array.from(this.sessions.keys());
await Promise.all(
activeSessions.map(sessionId =>
this.terminateSession(sessionId, 'SERVER_SHUTDOWN')
)
);
}
}
세션 보안 강화
1. CSRF 보호
class CSRFProtection {
private readonly csrfTokens = new Map<string, string>();
generateCSRFToken(sessionId: string): string {
const token = crypto.randomBytes(32).toString('base64url');
this.csrfTokens.set(sessionId, token);
return token;
}
validateCSRFToken(sessionId: string, providedToken: string): boolean {
const expectedToken = this.csrfTokens.get(sessionId);
if (!expectedToken || !providedToken) {
return false;
}
// 타이밍 공격 방지를 위한 상수 시간 비교
return crypto.timingSafeEqual(
Buffer.from(expectedToken),
Buffer.from(providedToken)
);
}
rotateCSRFToken(sessionId: string): string {
const newToken = this.generateCSRFToken(sessionId);
this.auditLogger.log('CSRF_TOKEN_ROTATED', {
sessionId: this.hashSessionId(sessionId)
});
return newToken;
}
}
2. 세션 데이터 암호화
class SessionDataEncryption {
private readonly encryptionKey: Buffer;
constructor(key: string) {
this.encryptionKey = crypto.scryptSync(key, 'session-salt', 32);
}
encryptSessionData(data: SessionData): EncryptedSessionData {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipher('aes-256-gcm', this.encryptionKey);
cipher.setAAD(Buffer.from('session-data'));
const serializedData = JSON.stringify(data);
let encrypted = cipher.update(serializedData, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
encryptedData: encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex'),
timestamp: Date.now()
};
}
decryptSessionData(encryptedData: EncryptedSessionData): SessionData {
const decipher = crypto.createDecipher('aes-256-gcm', this.encryptionKey);
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
decipher.setAAD(Buffer.from('session-data'));
let decrypted = decipher.update(encryptedData.encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return JSON.parse(decrypted);
}
}
3. 세션 무결성 검증
class SessionIntegrityValidator {
private readonly hmacKey: Buffer;
constructor(key: string) {
this.hmacKey = crypto.scryptSync(key, 'integrity-salt', 32);
}
generateSessionSignature(sessionData: SessionData): string {
const hmac = crypto.createHmac('sha256', this.hmacKey);
hmac.update(JSON.stringify(sessionData));
return hmac.digest('hex');
}
validateSessionIntegrity(sessionData: SessionData, signature: string): boolean {
const expectedSignature = this.generateSessionSignature(sessionData);
// 타이밍 공격 방지
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(signature, 'hex')
);
}
async detectSessionTampering(sessionId: string): Promise<boolean> {
const session = this.sessions.get(sessionId);
if (!session) {
return true; // 세션이 없으면 변조된 것으로 간주
}
const isValid = this.validateSessionIntegrity(session.data, session.signature);
if (!isValid) {
this.securityAlerts.emit('SESSION_TAMPERING_DETECTED', {
sessionId: this.hashSessionId(sessionId),
userId: session.data.userId
});
// 변조된 세션 즉시 무효화
await this.terminateSession(sessionId, 'INTEGRITY_VIOLATION');
}
return !isValid;
}
}
세션 모니터링 및 분석
1. 세션 활동 추적
class SessionActivityTracker {
private readonly activityLog = new Map<string, ActivityRecord[]>();
async trackActivity(sessionId: string, activity: SessionActivity): Promise<void> {
const session = this.sessions.get(sessionId);
if (!session) {
return;
}
const activityRecord: ActivityRecord = {
timestamp: Date.now(),
type: activity.type,
resource: activity.resource,
ipAddress: activity.ipAddress,
userAgent: activity.userAgent,
success: activity.success
};
// 활동 기록 저장
const activities = this.activityLog.get(sessionId) || [];
activities.push(activityRecord);
// 최근 100개 활동만 유지
if (activities.length > 100) {
activities.shift();
}
this.activityLog.set(sessionId, activities);
// 의심스러운 활동 패턴 감지
await this.detectSuspiciousActivity(sessionId, activities);
}
private async detectSuspiciousActivity(
sessionId: string,
activities: ActivityRecord[]
): Promise<void> {
const recentActivities = activities.filter(
activity => Date.now() - activity.timestamp < 5 * 60 * 1000 // 최근 5분
);
// 과도한 실패 시도
const failedAttempts = recentActivities.filter(activity => !activity.success);
if (failedAttempts.length > 10) {
await this.handleSuspiciousActivity(sessionId, 'EXCESSIVE_FAILURES');
}
// 비정상적인 접근 패턴
const uniqueIPs = new Set(recentActivities.map(activity => activity.ipAddress));
if (uniqueIPs.size > 3) {
await this.handleSuspiciousActivity(sessionId, 'MULTIPLE_IP_ADDRESSES');
}
// 빠른 연속 요청
const requestTimes = recentActivities.map(activity => activity.timestamp);
const avgInterval = this.calculateAverageInterval(requestTimes);
if (avgInterval < 100) { // 100ms 미만 간격
await this.handleSuspiciousActivity(sessionId, 'RAPID_REQUESTS');
}
}
private async handleSuspiciousActivity(
sessionId: string,
reason: string
): Promise<void> {
const session = this.sessions.get(sessionId);
if (session) {
this.securityAlerts.emit('SUSPICIOUS_SESSION_ACTIVITY', {
sessionId: this.hashSessionId(sessionId),
userId: session.userId,
reason,
timestamp: new Date().toISOString()
});
// 세션 일시 정지 또는 추가 인증 요구
session.requiresReauth = true;
session.suspiciousActivityCount = (session.suspiciousActivityCount || 0) + 1;
if (session.suspiciousActivityCount >= 3) {
await this.terminateSession(sessionId, 'REPEATED_SUSPICIOUS_ACTIVITY');
}
}
}
}
2. 세션 분석 및 리포팅
class SessionAnalytics {
async generateSessionReport(timeRange: TimeRange): Promise<SessionReport> {
const sessions = await this.getSessionsInRange(timeRange);
return {
totalSessions: sessions.length,
uniqueUsers: new Set(sessions.map(s => s.userId)).size,
averageSessionDuration: this.calculateAverageSessionDuration(sessions),
sessionsByHour: this.groupSessionsByHour(sessions),
topUserAgents: this.getTopUserAgents(sessions),
topIPAddresses: this.getTopIPAddresses(sessions),
securityIncidents: await this.getSecurityIncidents(timeRange),
suspiciousActivities: await this.getSuspiciousActivities(timeRange)
};
}
async detectAnomalousPatterns(): Promise<AnomalyReport[]> {
const anomalies: AnomalyReport[] = [];
// 비정상적인 세션 생성 패턴
const sessionCreationRate = await this.getSessionCreationRate();
if (sessionCreationRate > this.getBaselineCreationRate() * 3) {
anomalies.push({
type: 'HIGH_SESSION_CREATION_RATE',
severity: 'HIGH',
details: `Session creation rate: ${sessionCreationRate}/hour`
});
}
// 비정상적인 지역별 접근
const geographicDistribution = await this.getGeographicDistribution();
const unusualLocations = this.detectUnusualLocations(geographicDistribution);
if (unusualLocations.length > 0) {
anomalies.push({
type: 'UNUSUAL_GEOGRAPHIC_ACCESS',
severity: 'MEDIUM',
details: `Unusual access from: ${unusualLocations.join(', ')}`
});
}
return anomalies;
}
}
세션 보안 설정
1. 보안 정책 구성
interface SessionSecurityPolicy {
encryption: {
enabled: boolean;
algorithm: string;
keyRotationInterval: number;
};
expiration: {
absoluteTimeout: number;
idleTimeout: number;
renewalThreshold: number;
};
concurrency: {
maxSessionsPerUser: number;
allowMultipleDevices: boolean;
};
monitoring: {
trackActivity: boolean;
detectAnomalies: boolean;
alertThreshold: number;
};
}
const sessionPolicies: Record<string, SessionSecurityPolicy> = {
high_security: {
encryption: {
enabled: true,
algorithm: 'aes-256-gcm',
keyRotationInterval: 24 * 60 * 60 * 1000 // 24시간
},
expiration: {
absoluteTimeout: 8 * 60 * 60 * 1000, // 8시간
idleTimeout: 15 * 60 * 1000, // 15분
renewalThreshold: 2 * 60 * 1000 // 2분
},
concurrency: {
maxSessionsPerUser: 1,
allowMultipleDevices: false
},
monitoring: {
trackActivity: true,
detectAnomalies: true,
alertThreshold: 3
}
},
standard: {
encryption: {
enabled: true,
algorithm: 'aes-256-gcm',
keyRotationInterval: 7 * 24 * 60 * 60 * 1000 // 7일
},
expiration: {
absoluteTimeout: 24 * 60 * 60 * 1000, // 24시간
idleTimeout: 30 * 60 * 1000, // 30분
renewalThreshold: 5 * 60 * 1000 // 5분
},
concurrency: {
maxSessionsPerUser: 3,
allowMultipleDevices: true
},
monitoring: {
trackActivity: true,
detectAnomalies: true,
alertThreshold: 5
}
}
};
2. 환경별 설정
class SessionConfigManager {
private readonly config: SessionSecurityPolicy;
constructor(environment: string) {
this.config = this.loadConfigForEnvironment(environment);
}
private loadConfigForEnvironment(env: string): SessionSecurityPolicy {
const configs = {
production: sessionPolicies.high_security,
staging: sessionPolicies.standard,
development: {
...sessionPolicies.standard,
expiration: {
absoluteTimeout: 8 * 60 * 60 * 1000, // 8시간 (개발 편의)
idleTimeout: 60 * 60 * 1000, // 1시간
renewalThreshold: 10 * 60 * 1000 // 10분
},
monitoring: {
trackActivity: false,
detectAnomalies: false,
alertThreshold: 10
}
}
};
return configs[env] || configs.production;
}
getPolicy(): SessionSecurityPolicy {
return this.config;
}
}
관련 문서
- 토큰 위임 보안 - 토큰 위임 보안 고려사항
- 인증 패턴 - 전체 인증 패턴 개요
- 토큰 관리 아키텍처 - 토큰 관리 구현
- 통합 패턴 - 서비스 간 통합 보안