그누보드 최신버전 (5.1.1) 살펴보기 - bbs/visit_insert.inc.php 정보
그누보드5 그누보드 최신버전 (5.1.1) 살펴보기 - bbs/visit_insert.inc.php본문
그누보드 최신버전 (5.1.1) 살펴보기 - bbs/visit_insert.inc.php
bbs/visit_insert.inc.php 는 common.php에서 include 됩니다.
순수 방문자를 아이피를 기준으로 구분하여 날짜별로 저장하는 역활을 합니다.
// 방문자 상세 테이블
CREATE TABLE IF NOT EXISTS `g5_visit` (
`vi_id` int(11) NOT NULL default '0',
`vi_ip` varchar(255) NOT NULL default '',
`vi_date` date NOT NULL default '0000-00-00',
`vi_time` time NOT NULL default '00:00:00',
`vi_referer` text NOT NULL,
`vi_agent` varchar(255) NOT NULL default '',
PRIMARY KEY (`vi_id`),
UNIQUE KEY `index1` (`vi_ip`,`vi_date`),
KEY `index2` (`vi_date`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
`vi_id` 는 PRIMARY KEY 입니다. 정수형입니다.
`vi_ip` 는 접속한 클라이언트의 아이피를 저장합니다. varchar(15) 가 적당하다고 보여집니다. 255.255.255.255
`vi_date` 는 접속한 날짜를 저장합니다.
`vi_ip` 와 `vi_date` 를 UNIQUE KEY 로 지정함으로써 날짜별로 중복된 아이피가 없도록 합니다.
`vi_time` 는 접속한 날짜의 최초 시간을 저장합니다.
`vi_referer` 는 접속하기 직전에 직접 접속했는지 특정 링크를 클릭하여 들어왔는지 판별하기 위해 타고 들어온 url을 저장합니다.
`vi_agent` 는 접속한 클라이언트의 브라우져 종류를 저장합니다. 브라우져 마다 `나는 어떤 브라우져다` 라고 나타내는 값이 있는데 그것을 저장합니다.
//방문자 요약 테이블
CREATE TABLE IF NOT EXISTS `g5_visit_sum` (
`vs_date` date NOT NULL default '0000-00-00',
`vs_count` int(11) NOT NULL default '0',
PRIMARY KEY (`vs_date`),
KEY `index1` (`vs_count`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
`vs_date` 는 PRIMARY KEY 입니다. 중복이 있을 수 없으며, 날짜별로 접속 통계를 저장하기 위해서 사용되었습니다.
`vs_count` 는 해당 날짜의 방문자수 합계를 저장합니다.
원래는 `g5_visit` 만으로도 날짜별 방문자 집계를 구할수 있지만
로그 특성상 계속 수없이 많이 쌓이게 되므로, 날짜별 집계를 구할때 느려질수 있는 경우에 대비해서
`g5_visit_sum` 를 추가로 사용합니다.
function set_cookie($cookie_name, $value, $expire)
{
global $g5;
setcookie(md5($cookie_name), base64_encode($value), G5_SERVER_TIME + $expire, '/', G5_COOKIE_DOMAIN);
}
function get_cookie($cookie_name)
{
$cookie = md5($cookie_name);
if (array_key_exists($cookie, $_COOKIE))
return base64_decode($_COOKIE[$cookie]);
else
return "";
}
set_cookie는 쿠키명을 md5로 암호화 하고, 쿠키값을 base64_encode 로 변환하여 저장합니다.
get_cookie는 인수로 받은 쿠키명을 md5로 암호화 하고 그 쿠키명에 해당하는 쿠키값을 base64_decode 해서 다시 반환합니다.
쿠키라는 것은 클라이언트 컴퓨터에 브라우져가 저장하는 것이므로
사용자는 자신의 컴퓨터에 저장된 쿠키가 무엇인지 확인이 가능합니다.
경우에 따라 중요한 처리를 하기 위한 쿠키가 있기 때문에,
그 정보를 볼수있더라도 무슨 쿠키인지 값은 무엇인지 유추 하기 어렵도록 최소한의 변형을 주는 것입니다.
사실 md5로 쿠키명을 암호화 하더라도 그누보드와 같은 공개솔루션은
사용되는 쿠키명 자체를 알수 있기 때문에 충분히 md5의 값을 유추해내는 데는 어려움이 없습니다.
그러나 전문적이지 못한 사용자를 대상으로는 효과를 볼수 있는 방법입니다.
function escape_trim($field)
{
$str = call_user_func(G5_ESCAPE_FUNCTION, $field);
return $str;
}
함수명에서는 escape 하고 trim 을 사용할것 같지만 실제로는 escape 만 사용됩니다.
config.php 에서 정의된 G5_ESCAPE_FUNCTION 상수의 함수를 실행합니다.
기본값은 sql_escape_string 입니다.
function sql_escape_string($str)
{
if(defined('G5_ESCAPE_PATTERN') && defined('G5_ESCAPE_REPLACE')) {
$pattern = G5_ESCAPE_PATTERN;
$replace = G5_ESCAPE_REPLACE;
if($pattern)
$str = preg_replace($pattern, $replace, $str);
}
$str = call_user_func('addslashes', $str);
return $str;
}
config.php 에서 정의된 G5_ESCAPE_PATTERN, G5_ESCAPE_REPLACE 을 통해 치환을 거친후 addslashes 를 사용합니다.
현재 그누보드에서는 G5_ESCAPE_PATTERN, G5_ESCAPE_REPLACE 는 정의 되어있지 않습니다.
즉, 결국 addslashes 만 사용됩니다.
function clean_xss_tags($str)
{
$str = preg_replace('#</*(?:applet|b(?:ase|gsound|link)|embed|frame(?:set)?|i(?:frame|layer)|l(?:ayer|ink)|meta|object|s(?:cript|tyle)|title|xml)[^>]*+>#i', '', $str);
$search = array('"', "'");
$replace = array('"', ''');
$str = str_replace($search, $replace, $str);
return $str;
}
xss 공격에 사용될수 있는 태그를 제거하는 역활을 하는 함수입니다.
xss 란
Cross Site Scripting(XSS)의 약자이며, 특정페이지에 스크립트를 넣어서 세션을 가로채거나 공격자가 의도한대로 행동하도록 만드는 웹 공격의 일종입니다.
https://www.google.co.kr/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=xss%20%EB%9E%80
사실 디비에 저장하는 것 자체로는 아무 문제가 없는 부분이나 그것이 관리자 모드나 어딘가에 출력될때에 위험할수 있기 때문에 관련 태그를 제거하는 것이라고 볼수 있습니다.
엄밀히 말하면, 리퍼러나 에이전트 출력시에 < 만 치환 해주어도 아무 문제 없는 부분입니다.
오히려 데이타는 제대로 저장하고 출력시에만 < 만 치환 해준다면 어떤 공격이 어떤형태로 들어왔구나 하고 확인이 가능하므로 더 나은 방법이라고 볼수 있습니다.
function sql_query($sql, $error=G5_DISPLAY_SQL_ERROR)
{
global $g5;
// Blind SQL Injection 취약점 해결
$sql = trim($sql);
// union의 사용을 허락하지 않습니다.
//$sql = preg_replace("#^select.*from.*union.*#i", "select 1", $sql);
$sql = preg_replace("#^select.*from.*[\s\(]+union[\s\)]+.*#i ", "select 1", $sql);
// `information_schema` DB로의 접근을 허락하지 않습니다.
$sql = preg_replace("#^select.*from.*where.*`?information_schema`?.*#i", "select 1", $sql);
if ($error)
$result = @mysql_query($sql, $g5['connect_db']) or die("<p>$sql<p>" . mysql_errno() . " : " . mysql_error() . "<p>error file : {$_SERVER['SCRIPT_NAME']}");
else
$result = @mysql_query($sql, $g5['connect_db']);
return $result;
}
원래는 mysql_query 말고 다른 함수로 질의를 날릴수 있는 경우에 대비에 만들어진 함수 이지만
이후에 sql injection 공격에 대비한 부분이 포함되었습니다.
union 을 사용할수 없게 하는 것은 마음에 들지 않지만,
스킨이나 사용자에 의한 부분들도 있기 때문에 일반적으로 막기 위한 조치라고 볼수 있습니다.
sql injection 공격은
넘어온 파라미터를 검증없이 쿼리를 만드는 부분에 삽입함으로써 발생할 수 있습니다.
즉, 넘어온 파라미터만 검증을 잘해도 방어가 가능합니다.
function sql_fetch($sql, $error=G5_DISPLAY_SQL_ERROR)
{
$result = sql_query($sql, $error);
//$row = @sql_fetch_array($result) or die("<p>$sql<p>" . mysql_errno() . " : " . mysql_error() . "<p>error file : $_SERVER['SCRIPT_NAME']");
$row = sql_fetch_array($result);
return $row;
}
function sql_fetch_array($result)
{
$row = @mysql_fetch_assoc($result);
return $row;
}
sql_fetch 는 mysql_query 와 mysql_fetch_assoc 을 같이 실행 한 것이고
sql_fetch_array 는 mysql_fetch_assoc 만을 실행 한 것입니다.
if (get_cookie('ck_visit_ip') != $_SERVER['REMOTE_ADDR'])
방문자는 아이피를 기준으로 체크하기 때문에 get_cookie('ck_visit_ip') 와 접속 아이피가 다르다면 방문자 집계작업을 하겠다는 뜻입니다.
set_cookie('ck_visit_ip', $_SERVER['REMOTE_ADDR'], 86400);
접속 아이피를 24시간 동안 쿠키로 저장합니다. 다음 방문시 체크를 위한 부분입니다.
$tmp_row = sql_fetch(" select max(vi_id) as max_vi_id from {$g5['visit_table']} ");
$vi_id = $tmp_row['max_vi_id'] + 1;
테이블에서 `vi_id` 가 자동증가값으로 설정되어있지 않기 때문에 최대값에서 1을 더해 신규로 입력할 `vi_id`를 생성하는 부분입니다.
$remote_addr = escape_trim($_SERVER['REMOTE_ADDR']);
$_SERVER['REMOTE_ADDR'] 가 변조가 가능한지는 모르겠지만,
escape_trim 을 해주는 것보다 common.php 상단에서 $_SERVER['REMOTE_ADDR'] 의 형식 체크를 하는 것이 더 좋아보입니다.
$referer = "";
if (isset($_SERVER['HTTP_REFERER']))
$referer = escape_trim(clean_xss_tags($_SERVER['HTTP_REFERER']));
$user_agent = escape_trim(clean_xss_tags($_SERVER['HTTP_USER_AGENT']));
$sql = " insert {$g5['visit_table']} ( vi_id, vi_ip, vi_date, vi_time, vi_referer, vi_agent ) values ( '{$vi_id}', '{$remote_addr}', '".G5_TIME_YMD."', '".G5_TIME_HIS."', '{$referer}', '{$user_agent}' ) ";
$result = sql_query($sql, FALSE);
리퍼러와 에이전트 정보를 필터링을 거친 연후에 삽입합니다.
if ($result) {
$sql = " insert {$g5['visit_sum_table']} ( vs_count, vs_date) values ( 1, '".G5_TIME_YMD."' ) ";
$result = sql_query($sql, FALSE);
// DUPLICATE 오류가 발생한다면 이미 날짜별 행이 생성되었으므로 UPDATE 실행
if (!$result) {
$sql = " update {$g5['visit_sum_table']} set vs_count = vs_count + 1 where vs_date = '".G5_TIME_YMD."' ";
$result = sql_query($sql);
}
mysql_query 의 반환값은 성공시에는 리소스나 true, 실패시에는 false 를 반환합니다.
따라서
if ($result) {
보다는
if ($result !== false) {
가 적당하다고 보여집니다.
// 오늘
$sql = " select vs_count as cnt from {$g5['visit_sum_table']} where vs_date = '".G5_TIME_YMD."' ";
$row = sql_fetch($sql);
$vi_today = $row['cnt'];
// 어제
$sql = " select vs_count as cnt from {$g5['visit_sum_table']} where vs_date = DATE_SUB('".G5_TIME_YMD."', INTERVAL 1 DAY) ";
$row = sql_fetch($sql);
$vi_yesterday = $row['cnt'];
// 최대
$sql = " select max(vs_count) as cnt from {$g5['visit_sum_table']} ";
$row = sql_fetch($sql);
$vi_max = $row['cnt'];
// 전체
$sql = " select sum(vs_count) as total from {$g5['visit_sum_table']} ";
$row = sql_fetch($sql);
$vi_sum = $row['total'];
$visit = '오늘:'.$vi_today.',어제:'.$vi_yesterday.',최대:'.$vi_max.',전체:'.$vi_sum;
// 기본설정 테이블에 방문자수를 기록한 후
// 방문자수 테이블을 읽지 않고 출력한다.
// 쿼리의 수를 상당부분 줄임
sql_query(" update {$g5['config_table']} set cf_visit = '{$visit}' ");
환경설정 테이블에 기본 방문 통계를 집어넣는 부분입니다.
8
댓글 10개
제가 선생님의 강좌를 읽기 시작한 시점이요.
그 이전에도 보기는 했으나 사실 무슨 말씀인지 잘 몰랐습니다.
그러다.. 몰라도 좋다. 일단 읽기만이라도 해봐야지..
그러다 보면 언제인가는 들어오겠지..
이제는 한번 읽는 것만으로도 워낙 쉽게 풀어 주셔서 한 눈에 들어오고 있습니다.
이것이 참.. 대충 짜집기 하던 인간이 이제는 뭐라도 작은 것은
직접 만들 줄 알게 되었다는 것 아닙니까.
저는 반복하여 읽은 것 외, 특별하게 한 것은 없습니다.
너무 고맙습니다. 감사합니다!
눈이 뚫리셨군요. 좋은 현상인것 같습니다.
이제는 정말 한번이면 파악이 됩니다.
이제는 보입니다. ^^ 감사합니다!
부산사투리 입니다.
스크립 차단등등 외부 디비차단 등등 ^^
좋은 정보 감사 합니다.