운영 가이드에 적힌 디렉터리 구조를 보다가 업로드 폴더 위치가 눈에 띄었다. public/uploads/ 가 nginx 의 root 아래에 있었다. 그 위치면 주소만 알면 파일을 그대로 받을 수 있다.
DLP 포털은 PHP-FPM 뒤에서 도는 PHP 앱이고 정적 파일은 nginx 가 직접 내려 준다. 그러니 uploads/ 아래 파일도 PHP 를 거치지 않고 nginx 가 그대로 내려 줄 수 있는 자리였다.
Table of contents
Open Table of contents
배경 — 디렉터리 구조에서 본 것
다만 디렉터리가 그 아래 있다는 것만으로 열려 있다고 단정할 수는 없었다. location /uploads/ { deny all; } 처럼 그 경로만 막아 두는 것이 가능하기 때문이다.
그래서 sites-enabled/dlp-portal.conf 를 열어서 그 경로에 대한 규칙이 있는지를 확인했다. 위치가 아니라 설정이 실제로 무엇이 열려 있는지를 정한다.
검증 — 위치와 웹서버 설정
확인해 보니 별도 location 이 없어서 그 아래 있는 것이 전부 그대로 내려가고 있었다. 업로드된 파일 중에는 인적성검사 결과지처럼 아무나 받으면 안 되는 것도 섞여 있었다.
이 상태에서는 파일 이름을 알아내는 것만으로 접근이 된다. 이름이 result_20191203_1042.pdf 처럼 규칙적이면 추측도 된다. 그러니 이름을 어렵게 짓는 것으로는 부족했다.
조치 — 공개 자원과 보호 자원을 가른다
그래서 파일을 두 종류로 갈랐는데 누구나 봐도 되는 과정 이미지와, 권한이 있어야 하는 결과지·제출물이었다. 앞의 것은 그대로 두고 뒤의 것은 웹 루트 밖 /srv/dlp/storage/private/ 로 내렸다.
내려 주는 데는 두 가지 방법이 있었다. 하나는 PHP 가 권한을 확인한 뒤 readfile() 로 직접 흘려 주는 것이다. 다른 하나는 권한만 확인하고 X-Accel-Redirect 헤더로 nginx 에 넘기는 것이다. 뒤쪽을 골랐다. 이유는 앞쪽은 큰 파일을 보내는 동안 PHP-FPM 워커 하나가 계속 잡혀 있어서다. nginx 공식 문서에 따르면 internal 을 붙인 location 은 바깥 요청에 404 를 준다. X-Accel-Redirect 로 넘어온 요청만 그 location 이 받는다. 그래서 location /protected/ { internal; alias /srv/dlp/storage/private/; } 로 두면 주소를 알아도 권한이 없으면 못 받는다.
저장 파일명의 무작위화
저장할 때 파일 이름을 원본 그대로 쓰고 있던 것도 함께 손봤다. 원본 이름에는 사용자가 넣은 문자가 그대로 들어가서 인코딩 문제도 생기고 이름 자체가 정보를 흘리기도 한다.
그래서 저장 이름은 bin2hex(random_bytes(16)) 로 만든다. 원본 이름은 files.original_name 에 따로 뒀다. 내려 줄 때 그 이름을 Content-Disposition 헤더에 실어 주면 받는 쪽에서는 원래 이름으로 저장된다. 한글 이름은 RFC 6266 이 정한 대로 filename*=UTF-8''… 형태로 넣고 filename 을 예비로 함께 준다.
판단 기준 — 한 서버의 여러 사이트
작업하면서 한 서버에 server 블록이 셋(포털·관리자·API) 올라가 있는 것도 확인했다. 지금은 문제가 없지만 하나가 커지면 떼어 내야 할 수 있다.
그래서 무엇을 먼저 떼어 낼지를 미리 적어 두었는데, 근거는 접근 통제가 다른 것부터 갈라야 한다는 점이었다. 트래픽이 아니라 누가 접근할 수 있어야 하는지가 다른 관리자 사이트가 먼저 갈라져야 했다.
정리
- 업로드 경로가 열려 있는지는 위치가 아니라 nginx
location규칙으로 확인한다 - 파일을 공개 자원과 보호 자원으로 가른다
- 보호 자원은 웹 루트 밖에 두고
internal+X-Accel-Redirect로 내려 준다 - 주소를 알아도 권한이 없으면 못 받게 한다
- 그러면 경로 추측이 무의미해진다
- 저장 파일명을 원본이 아닌
random_bytes값으로 한다 - 원본 이름은 DB 에 두고 내려줄 때
Content-Disposition으로 준다 - 한 서버에 여러 사이트를 둘 때 접근 통제가 다른 것부터 뗀다