PHP 애플리케이션에서 세션 충돌은 여러 사용자나 요청 간에 세션 데이터가 섞이거나 덮어써지는 심각한 문제입니다. 다음은 세션 충돌의 주요 원인과 해결책들입니다.
문제: 공격자가 세션 ID를 고정하여 사용자 세션을 탈취
해결책:
// 로그인 성공 시 세션 ID 재생성
function secureLogin($username, $password) {
if (validateCredentials($username, $password)) {
session_regenerate_id(true); // 기존 세션 파일 삭제
$_SESSION['user_id'] = getUserId($username);
$_SESSION['login_time'] = time();
return true;
}
return false;
}
문제: 여러 애플리케이션이 동일한 세션 저장소를 공유하여 충돌
해결책:
// 애플리케이션별 세션 저장 경로 설정
ini_set('session.save_path', '/tmp/myapp_sessions');
ini_set('session.name', 'MYAPP_SESSIONID');
// 또는 세션 시작 전에 설정
if (!is_dir('/tmp/myapp_sessions')) {
mkdir('/tmp/myapp_sessions', 0755, true);
}
session_save_path('/tmp/myapp_sessions');
session_name('MYAPP_SESSIONID');
session_start();
문제: 여러 하위 도메인에서 세션 쿠키가 겹쳐서 충돌
해결책:
// 특정 도메인에만 쿠키 설정
session_set_cookie_params([
'lifetime' => 3600,
'path' => '/',
'domain' => '.myapp.com', // 특정 도메인 지정
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
session_start();
문제: 동시 요청에서 세션 데이터가 덮어써짐
해결책:
function safeSessionUpdate($callback) {
session_start();
// 세션 잠금을 위한 임시 파일 생성
$lockFile = sys_get_temp_dir() . '/session_' . session_id() . '.lock';
$lock = fopen($lockFile, 'w');
if (flock($lock, LOCK_EX)) {
try {
$callback();
session_write_close();
} finally {
flock($lock, LOCK_UN);
fclose($lock);
unlink($lockFile);
}
}
}
// 사용 예시
safeSessionUpdate(function() {
$_SESSION['counter'] = ($_SESSION['counter'] ?? 0) + 1;
});
문제: 동시 AJAX 요청으로 인한 세션 데이터 손실
해결책:
// 읽기 전용 세션 처리
function startReadOnlySession() {
session_start();
session_write_close(); // 즉시 세션 잠금 해제
}
// API 엔드포인트에서 사용
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
startReadOnlySession();
// 세션 데이터 읽기만 수행
echo json_encode(['user_id' => $_SESSION['user_id'] ?? null]);
}
문제: 세션 데이터 무결성 검증 부족으로 인한 보안 취약점
해결책:
function validateSession() {
if (!isset($_SESSION['user_id']) || !isset($_SESSION['login_time'])) {
return false;
}
// 세션 만료 시간 체크
if (time() - $_SESSION['login_time'] > 3600) {
session_destroy();
return false;
}
// IP 주소 검증 (선택적)
if (isset($_SESSION['ip_address']) && $_SESSION['ip_address'] !== $_SERVER['REMOTE_ADDR']) {
session_destroy();
return false;
}
return true;
}
문제: 기본 파일 기반 세션의 한계로 인한 충돌
해결책:
class DatabaseSessionHandler implements SessionHandlerInterface {
private $pdo;
public function __construct($pdo) {
$this->pdo = $pdo;
}
public function read($id) {
$stmt = $this->pdo->prepare("SELECT data FROM sessions WHERE id = ? AND expires > NOW()");
$stmt->execute([$id]);
return $stmt->fetchColumn() ?: '';
}
public function write($id, $data) {
$stmt = $this->pdo->prepare("REPLACE INTO sessions (id, data, expires) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 1 HOUR))");
return $stmt->execute([$id, $data]);
}
public function destroy($id) {
$stmt = $this->pdo->prepare("DELETE FROM sessions WHERE id = ?");
return $stmt->execute([$id]);
}
// 기타 필수 메서드들...
}
// 세션 핸들러 등록
session_set_save_handler(new DatabaseSessionHandler($pdo));
문제: 동일 사용자의 여러 탭에서 세션 정보가 충돌
해결책:
// 탭별 고유 식별자 생성
function generateTabId() {
return uniqid('tab_', true);
}
// 세션에 탭 정보 저장
if (!isset($_SESSION['tabs'])) {
$_SESSION['tabs'] = [];
}
$tabId = $_POST['tab_id'] ?? generateTabId();
$_SESSION['tabs'][$tabId] = [
'last_activity' => time(),
'data' => $_SESSION['tabs'][$tabId]['data'] ?? []
];
// 비활성 탭 정리
foreach ($_SESSION['tabs'] as $id => $tab) {
if (time() - $tab['last_activity'] > 1800) { // 30분
unset($_SESSION['tabs'][$id]);
}
}