Skip to content
isdnetworks
Go back

확장자와 내용이 달랐다

이미지 업로드에 확장자 검사만 있었는데 확장자만 이미지로 바꾼 파일이 올라왔다.

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 가 비거나 errorUPLOAD_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 를 주소로 직접 불러 보는 항목도 넣었다. 막았다고 생각하고 확인을 안 하면 실제로 막혔는지 모른다.

정리


Share this post on:

Previous Post
결제 통지가 두 번 왔다
Next Post
같은 비밀번호가 파일 세 개에 들어 있었다