보안에 있어 sql 이나 소스코드의 문제만은 아닙니다. 세션 데이터 노출 또한 위험성이 매우 높아 그 중요성이 부각되는데, 특히 공유호스팅 사용자라면 더욱 그러 합니다.
세션 데이터는 리눅스의 경우, /tmp 에 세션이 저장되는데, /tmp 폴더는 기본적으로 모든 사용자가 읽고, 쓰기가 가능한 권한을 가지고, 또 아파치도 여기에 세션 데이터 권한을 가지므로 그 위험은 커질 수 밖에 없습니다.
다음은 /tmp 폴더의 세션 데이터를 읽어 들이는 간단한 php 스크립트 예제입니다.
<?php
session_start();
$path = ini_get('session.save_path');
$handle = opendir($path);
while(false !== ($entry = readdir($handle))){
if(substr($entry, 0, 5) === 'sess_'){
if(($entry !== '.') && ($entry !== '..')) {
if($data = file_get_contents("$path/$entry")){
session_decode($data);
$session = $_SESSION;
$_SESSION = array();
echo "Session[".substr($entry, 5)."]\n";
print_r($session);
echo "<br />\n";
}
}
}
}
closedir($handle);
?>
이 스크립트로 본인의 도메인 세션 뿐 아니라 공유호스팅에 있는 다른 사용자의 모든 세션 데이터를 긁어 옵니다. 만약, 다른 도메인의 사용자가 비밀번호나 아이디를 세션으로 저장하고 관리하고 있다면, 이는 너무도 쉽게 비밀번호를 취득해 버립니다.
세션 삽입 공격
세션 데이터 저장소를 기본 임시폴더로 지정하지 않고, 임의 폴더(사용자 폴더)로 별도 관리하여 사용하더라도 그 폴더 또한, 읽기, 쓰기 권한이 있어 위험은 언제나 존재합니다. 세션 삽입 공격의 유형은 스크립트 내에 다른 사용자의 세션을 추가, 수정, 삭제하는 등 세션 변조로 악용할 가능성은 충분히 있습니다.
다음의 스크립트는 사용자의 기존 세션 데이터를 쉽고, 편리하게 수정할 수 있게 작성된 폼입니다.
<?php
session_start();
$path = ini_get('session.save_path');
function htmlchars($sessname){
return htmlentities($sessname, ENT_QUOTES, 'utf-8');
}
if(@$_POST['send'] === 'write'){
foreach($_POST as $key => $val){
if(@$_POST[$key] !== 'write'){
$_SESSION = $val;
$val = session_encode();
file_put_contents("$path/$key", $val);
print_r($val);
}
}
$_SESSION = array();
} else {
echo "<form action='' method='post'>\n";
echo "<input type='hidden' name='send' value='write'>";
$handle = opendir($path);
while(false !== ($entry = readdir($handle))){
if(substr($entry, 0, 5) === 'sess_'){
if(($entry !== '.') && ($entry !== '..')) {
if($data = file_get_contents("$path/$entry")){
session_decode($data);
$data = $_SESSION;
$_SESSION = array();
$sessname = htmlchars(substr($entry, 5));
echo "Session[".$sessname."]\n";
foreach($data as $key => $val){
$key = htmlchars($key);
$val = htmlchars($val);
echo $key.": <input type='text' name='".
$sessname."[".$key."]' value='".$val."'>\n";
}
echo "<br />\n";
}
}
}
}
closedir($handle);
echo "<input type='submit'>\n";
echo "</form>\n";
}
?>
이러한 스크립트의 실행 권한을 보통 막아 두어 관리자는 안심한다지만, 문제는 공격자가 파일업로드의 보안 취약점을 이용하여 우회하는 방법을 알게 되어, 업로드된 파일로부터 php 스크립트 사용 권한이 취득된다면, 문제는 달라 집니다. 그래서 가장 안전한 최상이라 할 수 있는 방법으로 디비로 관리하는 방법을 선택하는 것입니다.
session_set_save_handler 함수를 이용
bool session_set_save_handler ( callback $open , callback $close , callback $read , callback $write , callback $destroy , callback $gc )
이 함수를 이용한다면 파일이 아닌 디비로 세션 관리가 가능해지는데, 이 함수를 먼저 정의한 후에 session_start 함수를 뒤에 정의하면 디비로 관리되며, 이 함수를 정의하지 않은 폐이지는 일반 파일로 저장됩니다.
이 함수는 성공할 경우 TRUE를, 실패할 경우 FALSE를 반환합니다.
인 자 | 설 명 |
---|
open | 열기 함수. 클래스의 생성자처럼 작동하고 세션이 열릴 때 실행됩니다. 열기 함수는 두 인수를 받습니다. 첫 번째는 저장 경로이고 두 번째는 세션 이름입니다. |
---|
close | 닫기 함수. 클래스의 소멸자처럼 작동하고 세션 연산이 끝났을 때 실행됩니다. |
---|
read | 읽기 함수는 저장 핸들러가 정상적으로 작동하기 위해 항상 문자열 값을 반환해야 합니다. 읽을 데이터가 없으면 빈 문자열을 반환합니다. 다른핸들러에서 오는 값은 논리 표현으로 변환하여 반환합니다. 성공시엔 TRUE, 실패시엔 FALSE입니다. |
---|
write | "쓰기" 핸들러는 출력 스트림이 닫힐 때까지 실행되지 않습니다. 그러므로 "쓰기" 핸들러에서 디버그 구문 출력은 브라우저에서 볼 수 없습니다. 디버그 출력이 필요하면, 디버그 출력을 파일로 써야 합니다. |
---|
destroy | session_destroy()로 세션이 파괴될 때 실행되며, 세션 id를 인수로 받습니다. |
---|
gc | 쓰레기 수거자. 세션 쓰레기 수거가 실행될 때 실행되며, 최대 세션 수명을 인수로 받습니다. |
---|
다음 예제는 파일로 설정한 스크립트입니다.
<?php
function open($save_path, $session_name){
global $sess_save_path;
$sess_save_path = $save_path;
return(true);
}
function close(){
return(true);
}
function read($id){
global $sess_save_path;
$sess_file = "$sess_save_path/sess_$id";
return (string) @file_get_contents($sess_file);
}
function write($id, $sess_data){
global $sess_save_path;
$sess_file = "$sess_save_path/sess_$id";
if ($fp = @fopen($sess_file, "w")) {
$return = fwrite($fp, $sess_data);
fclose($fp);
return $return;
} else {
return(false);
}
}
function destroy($id){
global $sess_save_path;
$sess_file = "$sess_save_path/sess_$id";
return(@unlink($sess_file));
}
function gc($maxlifetime){
global $sess_save_path;
foreach (glob("$sess_save_path/sess_*") as $filename) {
if (filemtime($filename) + $maxlifetime < time()) {
@unlink($filename);
}
}
return true;
}
session_set_save_handler("open", "close", "read", "write",
"destroy", "gc");
session_start();
?>
디비로 스크립트를 제작하는데, 우선 테이블을 만들 필요가 있습니다. 테이블을 만들고, 다음 코드로 작성하면 나머지는 php 프로세서가 알아서 처리를 해주기 때문에, 사용자가 따로 작업할 필요가 없이 _SESSION 를 그대로 사용해도 됩니다.
<?php
define("DB_HOST_NAME", "localhost");
define("DB_USER", "디비 유저");
define("DB_PASS", "디비 패스워드");
define("SESSION_TABLE_NAME", "sessions");
function _open(){
global $conn;
if($conn = mysql_connect(DB_HOST_NAME, DB_USER, DB_PASS)){
return mysql_select_db(SESSION_TABLE_NAME, $conn);
}
return false;
}
function _close(){
return true;
}
function _read($id){
global $conn;
$id = mysql_real_escape_string($id);
$sql = "SELECT session_data FROM " . SESSION_TABLE_NAME .
" WHERE id='" . $id . "'";
if($result = mysql_query($sql, $conn)){
if(mysql_num_rows($result) == 0){
return false;
} else {
if($row = mysql_fetch_assoc($result)){
return $row['session_data'];
} else {
return false;
}
}
}
}
function _write($id, $data){
global $conn;
$expire = time();
$id = mysql_real_escape_string($id);
$expire = mysql_real_escape_string($expire);
$data = mysql_real_escape_string($data);
$sql = "REPLACE INTO " . SESSION_TABLE_NAME .
" VALUES ('$id', '$expire', '$data')";
if($row = mysql_query($sql, $conn){
return $row;
} else {
return false;
}
}
function _destroy($id){
global $conn;
$id = mysql_real_escape_string($id);
$sql = "DELETE FROM " . SESSION_TABLE_NAME . " WHERE id='$id'";
if($row = mysql_query($sql, $conn){
return $row;
} else {
return false;
}
}
function _gc($max){
global $conn;
$old_time = time()-$max;
$old_time = mysql_real_escape_string($old_time);
$sql = "DELETE FROM " . SESSION_TABLE_NAME .
" WHERE session_expire < '$old_time'";
if($row = mysql_query($sql, $conn){
return $row;
} else {
return false;
}
}
session_set_save_handler('_open', '_close', '_read', '_write',
'_destroy', '_gc');
session_start();
?>