캡챠는 사용자 입력 검증 관련된 개발하다보면 한번쯤 사용했거나 들어본적이 있을 것이다. 쉽게 설명하면 입력후 제줄된 문서를 진짜 타이핑해서 쓴건지 아니면 별도 프로그램으로 이용하여 제줄한건지 사전에 체크할 수 있는 시스템이다. 이러한 캡챠는 지속적으로 기술이 발전되고 있지만 자동입력 프로그램 또한 지속 발전하여 보안코드 마저 인식 후 뚫어버리는 경우가 있다.
구글같은 경우 여러 캡챠 기능들을 제공하는데 많이 사용한건 리캡챠v2 로 `로봇이 아닙니다` 가 나오는 캡챠가 아직까지 인기가 많다. 다른곳의 캡챠같은 경우 내 눈이 이상한지 몰라도 사람도 알아먹기 힘들정도로 문자를 생성하여 불편할때가 많다. 이렇다 보니 주로 구글 리캡챠v2 를 많이 선호하게 되는것같다.
본론으로 들어가 자체 캡챠의 경우 과거에 고객사 서버에서 구글 리캡챠 라이브러리를 못쓰는 특수 환경으로 인해 PHP+CSS+JAQUERY 를 이용하여 만들어 본적이 있는데 생각보다 스팸 글들이 잘막아져서 이번에 다시한번 만들어 보았다.
파일은 예제 특성상 총 4개로 구성되었으며 아래와 같다.
config.php | 본 캡챠 기능을 사용하기 위한 설정 및 함수 정의 |
index.php | 캡챠 예제 UI |
request-captcha.php | 캡챠 기능을 수행하기 위한 사전 준비 프로세서 |
request-send.php | 문서 제출 후 캡챠를 통한 체크 프로세서 |
| 테스트 바로가기
본 파일은 그대로 사용해도 되지만 기호상수인 `RI_CAPTCHA_SECRET_KEY` 값을 설정해 두는게 좋다.
<?php
session_start();
define('RI_CAPTCHA_SECRET_KEY', md5('비밀키')); // 고정된 비밀키 입력
// 엑세스토큰 자동생성
$_SESSION['access_token'] = md5($_SERVER['REMOTE_ADDR']);
// 랜덤문자열추출
function createRandTxt($clen = 6){
$txt = md5(time()).strtoupper(md5(time()));
$atxt = str_split($txt);
shuffle($atxt);
$rtxt = implode("",$atxt);
$mlen = strlen($rtxt)-$clen;
$slen = mt_rand(0,$mlen);
return mb_substr($rtxt, $slen,$clen);
}
해당 페이지는 캡챠 예제를 위한 UI 구성화면으로 실 사용시에는 필요한 부분만 참고하여 개발하면 된다.
<?php
// 캡챠 라이브러리로드
include __DIR__."/config.php";
?>
<!-- jquery 사용을 위한 라이브러리 로드 -->
<script src="//code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<!-- form -->
<form id="form" action="request-send.php" onsubmit="return false;">
<input type="hidden" name="access_token" value="<?=$_SESSION['access_token']?>">
<input type="hidden" name="secure" value="">
<div class="box input">
<input type="text" name="txt" value="" placeholder="질문을 입력해주세요.">
</div>
<div class="box">
<span class="captcha-txt">보안코드: </span>
</div>
<div class="box">
<input type="text" name="code" value="" placeholder="보안코드입력">
</div>
<input type="submit" value="제출" />
</form>
<script>
var authSubmit = false;
// 캡챠 사전 구성 프로그램 실행
function createCaptch(){
var $form = $('#form');
var access_token = $form.find('[name="access_token"]').val();
if(typeof access_token == 'undfined' || !access_token){ return false; }
// 기본 입력폼 초기화
$form.find('[name="secure"]').val('');
$form.find('[name="code"]').val('');
$form.find('[name="txt"]').val('');
$('#secure-captcha-txt').html('');
$.ajax({url:'request-captcha.php', data:{access_token:access_token} ,type:'post',dataType:'json'})
.done(function(e){
if(e.rst != 'success'){ alert(e.msg); return false; }
authSubmit = true;
$form.find('[name="secure"]').val(e.secure);
$('#secure-captcha-txt').html('.captcha-txt:after{content:"'+e.captchaTxt+'"; color:red;}');
})
.fail(function(e){
console.log(e.responseText);
alert("문서를 제출할 수 없습니다.");
});
}
// 서브밋
$(document).on('submit','#form',function(){
if(authSubmit !== true){ alert("문서를 제출할 수 없습니다."); return false; }
var $form = $('#form');
$.ajax({url:'request-send.php', data:$form.serialize() ,type:'post',dataType:'json'})
.done(function(e){
alert(e.msg);
if(e.rst != 'success'){return false; }
createCaptch();
})
.fail(function(e){
console.log(e.responseText);
alert("문서를 제출할 수 없습니다.");
})
.always(function(e){
authSubmit = true;
})
});
$(document).ready(function(e){
createCaptch();
});
</script>
<style>
#form { width:100%; max-width:400px; margin:0 auto; }
#form .box{ margin:5px 0; padding:5px 0; border-bottom:solid 1px #eee;}
</style>
<!-- 캡챠로 생성된 랜덤 문자를 출력하기 위한 용도 (css 특성상 jquery 로는 after 선택자를 사용할 수 없기때문에 이와 같이 처리) -->
<style type="text/css" id="secure-captcha-txt"></style>
해당 파일은 캡챠 사용을 위한 사전 준비 프로그램이라고 보면 된다.
<?php
// 캡챠 라이브러리로드
include __DIR__."/config.php";
extract($_POST);
$response = array('rst'=>'fail','msg'=>'서버오류');
try{
if(empty($access_token)){ throw new Exception("엑세스토큰이 누락되었습니다.");}
if( $access_token != $_SESSION['access_token']){ throw new Exception("엑세스토큰이 일치하지 않습니다."); }
// 성공시 secure/captchaTxt 값을 내려준다.
$response['captchaTxt'] = createRandTxt();
$response['secure'] = md5(RI_CAPTCHA_SECRET_KEY.$response['captchaTxt']);
$response['rst'] = 'success';
}
catch(Exception $e){
$response['rst'] = 'fail';
$response['msg'] = $e->getMessage();
}
die(json_encode($response));
해당 파일은 최종 form 에서 문서 제출에 대한 검증을 캡챠와 함께 처리 하는 프로세서 이다.
<?php
// 캡챠 라이브러리로드
include __DIR__."/config.php";
extract($_POST);
$response = array('rst'=>'fail','msg'=>'서버오류');
try{
if(empty($access_token)){ throw new Exception("엑세스토큰이 누락되었습니다.");}
if( $access_token != $_SESSION['access_token']){ throw new Exception("엑세스토큰이 일치하지 않습니다."); }
// 필수값 체크
if(empty($txt)){ throw new Exception("질문을 입력해 주세요."); }
if(empty($code)){ throw new Exception("보안코드를 입력해주세요."); }
if(empty($secure)) { throw new Exception("보안키값이 누락되었습니다."); }
// 보인키 검증
$checkSecure= md5(RI_CAPTCHA_SECRET_KEY.$code);
if($secure != $checkSecure){ throw new Exception("보안키값이 일치하지 않습니다."); }
// 성공시 secure/captchaTxt 값을 내려준다.
$response['rst'] = 'success';
$response['msg'] = '질문이 제출되었습니다.';
}
catch(Exception $e){
$response['rst'] = 'fail';
$response['msg'] = $e->getMessage();
}
die(json_encode($response));
여기서 중요한점은 code 를 직접 비교하지 않고 secure 코드를 이용하여 비교를 했다는점이다. secure 자체가 code와 비밀키를 합쳐서 생성했기때문에 넘겨 받은 코드와 secure 가 다르다면 변조된 것으로 판단되기 때문이다. 여기서 보안을 더 강화한다면 secure 값을 세션을 넣어서 아래와 같이 한번 더 체크하는 구문을 넣어주면 된다.
| request-captcha.php 소스
// 성공시 secure/captchaTxt 값을 내려준다.
$response['captchaTxt'] = createRandTxt();
$response['secure'] = md5(RI_CAPTCHA_SECRET_KEY.$response['captchaTxt']);
$response['rst'] = 'success';
// 보안코드 변조를 위한 추가 소스
$_SESSION['secure'] = $response['secure'];
| request-send.php 소스
// 보안코드 변주를 위한 추가 소스
if( $_SESSION['secure'] != $secure){ throw new Exception("보안키값이 변조되었습니다."); }
unset($_SESSION['secure']);
위와 같이 자체 캡챠를 통해서도 어느정도 막을 수 있으며 한가지 포인트가 있다면 CSS의 after 선택자를 통해 html 문서상 생성된 캡챠 코드를 막았다는 점이다. 이런 부분들은 아무리 자동화된 프로그램이라도 조건이나 환경이 달라지게 되면 그만큼에 대한 처리를 해줘야하기때문에 공격자 입장에서는 포기할때가 많을 것이다.