본문으로 건너뛰기
개발 머꼬
개발 노트보안
hohyeon.dev24

업로드 파일 서빙 경로에 ../를 넣었더니 서버 설정 파일이 내려간 이유

  • #Engineering Note
  • #보안

문제 발생

업로드된 이미지를 서빙하는 라우트를 이렇게 만들었습니다.

const filePath = path.join(uploadRoot(), ...params.path);
return new Response(await readFile(filePath));

/uploads/../../.env 같은 요청에 서버의 환경 파일이 그대로 응답으로 나갔습니다.

원인 분석

..는 경로 문법의 일부입니다. OWASP는 이 공격을 이렇게 정의합니다 — "dot-dot-slash(../)" 시퀀스로 파일을 참조하는 변수를 조작해, 웹 루트 폴더 바깥에 저장된 파일과 디렉터리에 접근하는 것을 목표로 하는 공격입니다.

path.join은 보안 함수가 아닙니다. ..를 만나면 규칙대로 상위로 올라간 정상적인 경로를 만들어줄 뿐입니다.

인코딩 변형도 함께 봐야 합니다. OWASP가 나열하는 것만 해도 %2e%2e%2f, ..%2f, ..%5c, ..%c0%af, 그리고 널 바이트(%00)까지 있습니다. 문자열에서 ".."만 걸러내는 방식이 실패하는 이유입니다.

공격자가 얻는 것도 명시돼 있습니다 — 애플리케이션 소스 코드나 설정, /etc/passwd 같은 중요한 시스템 파일을 포함해 파일 시스템에 저장된 임의의 파일과 디렉터리입니다.

해결 방안

  1. 정규화한 뒤 루트 안에 있는지 확인합니다. 이것이 핵심이고, 나머지는 보조 수단입니다.
const root = path.resolve(uploadRoot());
const filePath = path.resolve(root, ...parts);
if (filePath !== root && !filePath.startsWith(root + path.sep)) return null;

resolve..와 인코딩 해제 후의 결과까지 모두 정리한 최종 경로를 만들고, 그 경로가 루트 밖이면 거절합니다. OWASP의 권고 그대로 파일 IO API에 쓰기 전에 입력을 정규화하는 것입니다.

  1. 사용자가 경로 전체를 정하지 못하게 합니다. OWASP가 먼저 드는 방어입니다 — 사용자가 경로의 모든 부분을 제공할 수 없게 하고, 우리 코드로 감쌉니다. 파일명 대신 DB의 id를 받아 실제 경로는 서버가 조회하는 방식이 가장 안전합니다.

  2. 알려진 좋은 값만 허용합니다. 확장자·문자 집합·세그먼트 수를 화이트리스트로 제한합니다. 차단 목록은 인코딩 변형에 계속 집니다.

  3. 심볼릭 링크도 고려합니다. 루트 안의 파일이 밖을 가리킬 수 있습니다. 업로드 디렉터리에 링크 생성을 허용하지 않거나 realpath까지 확인합니다.

  4. 응답 헤더를 함께 잠급니다. Content-Type을 신뢰할 수 있는 값으로 고정하고 X-Content-Type-Options: nosniff를 붙입니다 — 업로드된 파일이 브라우저에서 스크립트로 해석되는 별개의 문제를 막습니다.

  5. 테스트에 공격 문자열을 넣습니다. ../, %2e%2e%2f, 절대 경로, 널 바이트를 케이스로 두면 이 방어가 나중에 조용히 사라지지 않습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.