PHP 시큐어코딩
해당글은
박찬빈. "OWASP top 10 대응 PHP 시큐어 코딩 규칙 연구." 국내석사학위논문 고려대학교 컴퓨터정보통신대학원, 2019. 서울 논문을 참고해서 만들어졌습니다.
http://www.riss.or.kr/search/detail/DetailView.do?control_no=29f9b7a554271222ffe0bdc3ef48d419&keyword=OWASP+top+10+%EB%8C%80%EC%9D%91+PHP+%EC%8B%9C%ED%81%90%EC%96%B4+%EC%BD%94%EB%94%A9+%EA%B7%9C%EC%B9%99+%EC%97%B0%EA%B5%AC&p_mat_type=be54d9b8bc7cdb09
www.riss.or.kr
1. 인젝션
인젝션이란?
사이트의 보안 허점을 이용해 특정 SQL 쿼리문을 전송해 공격자가 원하는 DB의 중요한 정보를 가져오는 해킹 기법이다.
대부분 클라이언트가 입력한 데이터를 제대로 필터링하지 못하는 경우에 발생하며, 가장 쉬우면서도 치명적이기 때문에 위험도가 높은편. 그래서 Prepared-Statement방식을 사용하도록 권고하고 있다.
위험한 쿼리를 사용한 예시)
위의 경우는 검증이나 이스케이프 없이 쿼리문에 들어갈 경우 인젝션에 매우 취약한 방식입니다.
그래서 PDO라고하는 데이터베이스 라이브러리를 사용을 권고하고 있습니다.
PDO를 사용한 예시)
<?php
$host = "localhost";
$dbname= "test";
$username = "root";
$password = "";
try{
$conn = new PDO("mysql:host=$host;dbname=$dbname", $username, $password);
$query = " SELECT * FROM table WHERE id = :id ";
$stmt = $pdo->prepare($query);
$stmt->bindParam(":id", $_GET['id'], PDO::PARAM_INT);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
echo $row['content'];
}catch(PDOException $e){
echo "PDO 연결 실패" . $e->getMessage();
}
?>
부연 설명을 하자면 $_GET['id']를 정수형으로 받아 :id 변수로 바인딩한다.
execute함수로 바인딩한 :id를 fetch함으로써 결과를 반환받는다.
PDO::PARAM_INT가 정수형이 아닌 텍스트라면 PDO::PARAM_STR 등 적절한 자료형으로 변경해주면 된다.
PARAM_BOOL | 불리언 | PARAM_NULL | Null |
PARAM_INT | 정수 | PARAM_STR | 문자열 |
추가적으로 파라미터 검증과 Filter함수를 통해 다른 공격을 막는것도 좋다
<?php
$text = "test@test.com";
var_dump(filter_var($text, FILTER_SANITIZE_EMAIL)); // test@test.com"
$text = "t이메일 est@test.com";
var_dump(filter_var($text, FILTER_SANITIZE_EMAIL)); // test@test.com"
$HTML = "<a href='#'> anchor tag </a>";
$HTML2 = filter_var($HTML, FILTER_SANITIZE_STRING);
$HTML3 = filter_var($HTML, FILTER_SANITIZE_SPECIAL_CHARS);
var_dump($HTML); ## 실제 A태그 HTML 형식으로 노출
var_dump($HTML2); ## anchor tag 텍스트만 노출
var_dump($HTML3);
##<a href='#'> anchor tag </a> HTML태그가 적용되지 않고 순수 태그들만 텍스트로 노출
?>
FILTER_SANITIZE_EMAIL | 알파벳, 숫자, !#$%&'*+-=?^_ {|}~@. []를 제외한 모든 문자 제거 |
FILTER_SANITIZE_ENCODE D | URL-encode하여 패러미터값으로 사용 할 수 있게 함. |
FILTER_SANITIZE_MAGIC_Q UOTES | addslashes() 함수를 적용하여 홋따옴표 (‘), 쌍따옴표(“), 백슬래시(\), Nul 앞에 백슬래시를 붙여 이스케이프함. |
FILTER_SANITIZE_NUMBER _FLOAT | 숫자와 + - 기호를 제외한 모든 문자를 제거함. 설정에 따라 .,eE 허용 가능 |
FILTER_SANITIZE_NUMBER _INT | 숫자와 + - 기호를 제외한 모든 문자를 제거함. |
FILTER_SANITIZE_SPECIAL _CHARS | htmlspecialchars() 함수를 적용하여 " <>&를 HTML 엔티티로 치환하거나 제 거함.. |
FILTER_SANITIZE_FULL_SP ECIAL_CHARS | htmlspecialchars() 함수에 ENT_QUOT ES 옵션을 적용하여 '"<>&를 HTML 엔 티티로 치환함. |
FILTER_SANITIZE_STRING | 문자열에서 모든 HTML 태그를 제거함. Flag를 부여하여 특수문자를 치환하거나 제거함. |
FILTER_SANITIZE_URL | 알파벳, 숫자와 $-_.+!*'(),{}|\\^~[] <> #%";/?:@&= 를 제외한 모든 문자를 제 거함. |
FILTER_VALIDATE_DOMAIN | 유효한 도메인 이름 형식 검증 |
FILTER_VALIDATE_EMAIL | 유효한 이메일 형식 검증 |
FILTER_VALIDATE_INT | 정수 값 검증 |
FILTER_VALIDATE_IP | IP 주소 형식 검증(v4, v6) |
FILTER_VALIDATE_URL | URL 형식 검증 |
2. 취약한 인증과 세션관리
취약한 인증의 대표적인 사례는 너무 쉬운 비밀번호로 사용자 계정을 생성해 무작위 대입 공격이나 타사이트 계정 정보로 로그인을 시도하는 경우가 있다. 이를 대비해 비밀번호에 대한 조건을 만드는 것
계정명 | 알파벳은 모두 소문자 처리 |
비밀번호 길이 | 10~128자 사이 |
비밀번호 복잡도 | 1개 이상의 대문자 포함 1개 이상의 소문자 포함 1개 이상의 숫자 포함 1개 이상의 특수문자 포함(공백 제외) |
인증 시도 시 계정명이나 비밀번호가 잘못되어 실패했을 경우 표시되는 에러 메시지도 상세히 설명할 필요가 없다.
(계정 정보를 유추할 수 있기때문)
또한 php.ini에서 session_use_strict_mode를 활성화해 세션 고정을 막아 초기화하지 않은 세션 아이디를 허용하지 않게 한다.
3. 민감한 데이터 노출
주민등록번호나 비밀번호등 민감할 수 있는 개인 정보는 꼭 암호화를 하자.
password_hash와 password_verify함수를 통해 안전하게 암호화를 할 수 있다.
$pwd = "패스워드입니다";
$encrypt = password_hash($pwd, PASSWORD_DEFAULT); ## bcrypt가 사용된 알고리즘
echo $encrypt;
## $2y$10$WBian.VxOQ2brA49g6FoJu7.Hde94PQaMNZmpUe7d8wIrh2IED38m 암호는 고정으로 암호화 되지 않음.
var_dump(password_verify($pwd,$encrypt)); ## TRUE OR FALSE 리턴
4. XML 외부 개체(XXE)
libxml_disable_entity_loader(true) 를 통해 외부 개체 공격을 회피할 수 있다.
PHP8 이전은
if (\PHP_VERSION_ID < 80000) {
libxml_disable_entity_loader(true);
}
5. 취약한 접근 통제
디렉터리의 위치를 참고할 수 있으니 내장 함수인 basename()과 realpath()를 사용해서 접근하지 못하도록 제한하는 것이 좋다.
$FILE = "../../kakao.img";
echo $FILE; ## ../../kakao.img
echo basename(realpath($FILE)); ## kakao.img. but 존재하지 않으면 x
6. 크로스 사이트 스크립팅(XSS)
XSS는 공격자가 입력한 데이터가 서버를 거쳐 다른 사용자에게 표시될 때 발생할 위험이 있는 취약점이다.
예를 들어 어떤 폼 작성을 통해서 해당 스크립트가 DB에 기록되고, 특정 사용자가 해당 DB에 접근할 경우 해당 스크립트가 실행되어 사용자 브라우저에 접근이 가능하다. 이를 방지하고자 htmlentities함수를 사용하거나 또는 HTMLPurifier 라이브러리를 사용하자.
$Tag = "<script>alert('코드 실행!');</script>";
echo $Tag; ## 코드 실행 됨.
$xss = htmlentities($Tag, ENT_QUOTES | ENT_HTML5, 'UTF-8');
echo $xss; ## 스크립트 실행 안됨
$xss = htmlspecialchars($Tag, ENT_QUOTES, 'UTF-8');
echo $xss; ## 스크립트 실행 안됨
7. 크로스 사이트 요청 변조(CSRF)
CSRF는 공격자가 피해자에게 사이트 내에서 XSS를 이용한 악성 요청 양 식이나 피싱사이트 등을 이용하도록 유도하여, 피해자가 의도하지 않은 요청 을 서버에 전송하는 공격이다. 이를 해결하는 방법은 Referer 검증과 CSRF 토큰 검증이 있다.
Referer 검증
Referrer 검증은 요청을 전송한 사이트의 도메인과 받는 도메인이 동일한 지 체크하는 방식이다.
if(strpos($_SERVER['HTTP_REFERRER'], $SERVER['HTTP_HOST'])){
## 실행
}
CSRF 토큰 검증
CSRF 토큰 검증은 세션에 CSRF 토큰을 생성해두고, 요청 시 CSRF 토 큰을 전송한 후, 요청 받은 페이지에서 세션의 CSRF 토큰과 전송된 CSRF 토큰 값을 비교하는 방법이다
<?
## form.html
$Token = bin2hex(openssl_random_pseudo_bytes(32));
## ex) 144e2b091f77ae4eff73afff717431bb0cf27984b719ec14a13174214fb634e6 값 생성
$_SESSION['csrfToken'] = $Token;
?>
<form action="csrf_test.php" method="post">
<input type="hidden" name="csrfToken" value="<?=$Token?>" />
<input type="text" name="title" value="test" />
<button type="submit">전송</button>
</form>
<?
##csrf_test.php
if($_POST['csrfToken'] === $_SESSION['csrfToken']){
## 인증 성공!
}
?>
이 외에도 검증되지 않은 리다이렉트나 설정 등 여러 내용이 있는데, 시큐어 코딩에 관심이 있다면 해당 논문을 보는 것도 추천드립니다.