본문으로 건너뛰기

세션 관리 보안

개요

세션 관리는 사용자 인증 상태를 유지하고 보안을 보장하는 핵심 요소입니다. 특히 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;
}
}

관련 문서