본문으로 건너뛰기

세션 관리 보안

개요​

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

관련 문서​