이미지 업로드에 확장자 검사만 있었는데 확장자만 이미지로 바꾼 파일이 올라왔다.
Table of contents
Open Table of contents
확장자는 이름의 일부일 뿐이다
올리는 쪽이 파일명을 정하므로 확장자는 내용과 아무 관계가 없다. 내용을 읽어 판단해야 했다.
$info = @getimagesize($_FILES['img']['tmp_name']);
if ($info === false) {
return '이미지 파일이 아닙니다';
}
$allowed = [IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_GIF];
if (!in_array($info[2], $allowed, true)) {
return '지원하지 않는 형식입니다';
}
getimagesize 는 파일 앞부분의 표지를 읽어 형식을 판단한다. 이미지가 아니면 false 를 돌려주고 맞으면 IMAGETYPE_JPEG 같은 상수를 세 번째 자리에 담아 준다. 확장자 대신 그 값으로 가른다.
이름을 새로 만들었다
원본 이름을 저장 이름으로 쓰면 이름 자체에 경로 문자가 들어올 수 있다. 이름을 우리가 만들었다.
$ext = image_type_to_extension($info[2], false);
$name = date('Ymd') . '_' . bin2hex(openssl_random_pseudo_bytes(8)) . '.' . $ext;
확장자도 올린 쪽 이름이 아니라 image_type_to_extension 으로 판별한 형식에서 가져온다. 임의 값은 openssl_random_pseudo_bytes 로 만들었다. 올린 쪽이 정한 문자열이 저장 경로에 하나도 안 들어가는 상태가 됐다.
실행 가능한 자리에 안 뒀다
업로드 폴더가 DOCROOT 안에 있었다. 거기에 실행 가능한 파일이 있으면 주소로 부를 수 있다.
폴더를 DOCROOT 밖으로 옮기고 readfile 로 내보내게 했다. 옮기지 못하는 경우를 위해 .htaccess 로 실행도 막았다.
<Directory /var/www/html/upload>
php_flag engine off
Options -ExecCGI
AddType text/plain .php .phtml .php3 .php4 .php5
</Directory>
여러 겹으로 막은 것은 하나가 빠져도 나머지가 막게 하려는 것이었다. 설정 파일 한 줄에 기대면 그 줄이 사라졌을 때 아무것도 안 남는다.
크기 제한과 재저장
크기 제한이 화면에만 있어서 서버에도 넣었다. 그러면서 서버 설정 한도도 함께 봤다.
upload_max_filesize = 5M
post_max_size = 8M
upload_max_filesize 가 코드의 한도보다 작으면 코드에 닿기 전에 잘린다. 그때는 $_FILES 가 비거나 error 에 UPLOAD_ERR_INI_SIZE 가 들어온다. 그 값을 보고 안내를 다르게 했다.
가장 확실한 방법은 받은 이미지를 다시 만드는 것이었다.
$img = imagecreatefromstring(file_get_contents($tmp));
if ($img === false) return '이미지를 읽을 수 없습니다';
imagejpeg($img, $target, 85);
imagedestroy($img);
imagecreatefromstring 으로 읽고 imagejpeg 로 다시 쓰면 픽셀만 남는다. 파일 안에 숨은 것이 있어도 없어지고 용량도 줄고 형식도 통일된다. IMAGETYPE_PNG 처럼 투명도가 있는 것은 imagepng 으로 유지해야 해서 조건을 따로 뒀다.
확인 목록에는 정상 이미지와 확장자만 이미지인 파일과 크기 초과와 빈 파일과 ../ 가 든 파일명을 넣었다. 마지막으로 업로드 폴더의 .php 를 주소로 직접 불러 보는 항목도 넣었다. 막았다고 생각하고 확인을 안 하면 실제로 막혔는지 모른다.
정리
- 확장자는 올리는 쪽이 정하는 문자열이라 내용과 무관하다
getimagesize로 형식을 판단하고image_type_to_extension으로 확장자를 가져온다- 원본 이름을 안 쓰고
openssl_random_pseudo_bytes로 저장 이름을 만든다 - 업로드 폴더를
DOCROOT밖에 두고 못 옮기면.htaccess로 실행을 막는다 - 한 줄에 기대지 말고 여러 겹으로 막는다
upload_max_filesize에 먼저 걸리면UPLOAD_ERR_INI_SIZE가 온다imagecreatefromstring과imagejpeg로 다시 쓰면 픽셀만 남는다- 확인 목록에 업로드 폴더의
.php를 직접 불러 보는 항목을 넣는다