파일 업로드 취약점이란? 웹쉘 업로드를 차단하는 보안 설정

파일 업로드 취약점이 발생하는 원인과 웹쉘 공격 과정, 취약한 코드 및 확장자 검사·저장 경로 분리·실행 권한 차단 방법을 알아봅니다.

게시판 첨부파일, 프로필 사진, 이력서 제출처럼 파일을 올리는 기능은 대부분의 웹사이트에서 사용된다. 사용자는 파일 하나를 선택할 뿐이지만 서버에서는 파일의 이름과 형식을 확인하고, 지정된 경로에 저장한 뒤 다시 내려받을 수 있도록 처리한다.

이 과정에서 파일을 제대로 검사하지 않으면 이미지나 문서 대신 서버에서 실행되는 스크립트가 저장될 수 있다. 특히 업로드된 파일이 웹에서 직접 접근할 수 있는 경로에 저장되고 실행 권한까지 가지고 있다면 웹쉘 공격으로 이어질 가능성이 커진다.

파일 업로드 취약점이란?

파일 업로드 취약점은 사용자가 전달한 파일의 이름, 확장자, 내용과 저장 경로를 안전하게 검증하지 않아 위험한 파일이 서버에 저장되거나 실행되는 보안 문제다.

정상적인 이미지 파일이 업로드되는 것 자체는 문제가 되지 않는다. 문제는 공격자가 실행 가능한 스크립트를 업로드하고, 저장된 파일의 주소로 접근했을 때 웹 서버가 해당 파일을 프로그램으로 처리하는 경우다.

웹쉘은 웹 요청을 통해 서버에서 파일 열람이나 명령 실행 등의 기능을 수행하는 악성 스크립트다. 웹쉘이 실행되면 홈페이지 변조, 설정 파일 유출, 데이터베이스 접속 정보 탈취, 추가 악성파일 설치 등으로 피해가 확대될 수 있다.

파일 업로드 취약점의 핵심은 외부에서 받은 파일이 검증 없이 실행 가능한 위치에 저장된다는 점이다.

파일 업로드 취약점이 발생하는 원인

가장 흔한 원인은 원본 파일명과 확장자를 그대로 신뢰하는 것이다. 사용자가 전달하는 파일명은 서버가 만든 값이 아니므로 얼마든지 조작될 수 있다.

특정 확장자만 차단하는 방식도 안전하지 않다. 서버의 종류와 설정에 따라 실행 가능한 확장자가 달라질 수 있고, 대소문자나 다중 확장자 처리 방식에서도 차이가 발생하기 때문이다. 위험한 확장자를 하나씩 찾아 차단하는 것보다 실제 서비스에 필요한 확장자만 허용하는 방식이 안전하다.

HTTP 요청에 포함된 Content-Type 값만 확인하는 것도 부족하다. 이 값은 파일을 보내는 쪽에서 변경할 수 있으므로 image/jpeg라고 표시됐다고 해서 실제 내용까지 JPEG 이미지라고 단정할 수 없다.

업로드 디렉터리가 웹 루트 안에 있고 해당 경로에서 PHP, JSP 등의 스크립트 실행이 허용된 경우에는 위험성이 더욱 커진다. 파일 검사가 한 번만 우회돼도 업로드된 파일이 서버에서 실행될 수 있기 때문이다.

파일 업로드에 취약한 Java 코드

다음은 사용자가 올린 파일을 원본 이름 그대로 웹 경로에 저장하는 코드다.

@PostMapping("/upload")
public String upload(@RequestParam MultipartFile file)
        throws IOException {

    String fileName = file.getOriginalFilename();
    Path savePath = Paths.get(
        "/var/www/html/uploads/" + fileName
    );

    file.transferTo(savePath);

    return "/uploads/" + fileName;
}

이 코드는 파일 크기와 실제 형식을 확인하지 않는다. getOriginalFilename()으로 받은 이름을 그대로 저장 경로에 사용하며, 저장 위치도 웹에서 직접 접근할 수 있는 /var/www/html/uploads/다.

만약 해당 디렉터리에서 서버 측 스크립트가 실행된다면 공격자가 올린 파일이 웹쉘로 동작할 수 있다. 파일명에 경로 관련 문자열이 포함될 경우에는 원래 의도한 디렉터리가 아닌 다른 위치에 저장되는 문제도 발생할 수 있다.

웹쉘 업로드를 차단하는 안전한 처리 방법

가장 먼저 적용해야 할 방법은 사용자가 보낸 원본 파일명을 실제 저장에 사용하지 않는 것이다. 서버에서 UUID나 충분히 예측하기 어려운 난수를 이용해 새로운 파일명을 만들어야 한다.

이미지 업로드 기능이라면 확장자만 검사하지 말고 이미지 처리 라이브러리를 이용해 실제 이미지로 열리는지 확인하는 것이 좋다. 정상적으로 해석된 이미지를 PNG나 JPG 형식으로 다시 인코딩해 저장하면 원본 파일에 포함된 불필요한 데이터를 제거하는 데도 도움이 된다.

if (file.isEmpty() || file.getSize() > 5 * 1024 * 1024) {
    throw new IllegalArgumentException("허용되지 않은 파일입니다.");
}

BufferedImage image = ImageIO.read(file.getInputStream());

if (image == null) {
    throw new IllegalArgumentException("이미지 형식이 아닙니다.");
}

String storedName = UUID.randomUUID() + ".png";
Path uploadDir = Paths.get("/srv/app-uploads")
        .toAbsolutePath()
        .normalize();

Files.createDirectories(uploadDir);

Path target = uploadDir.resolve(storedName).normalize();

if (!target.getParent().equals(uploadDir)) {
    throw new SecurityException("잘못된 저장 경로입니다.");
}

ImageIO.write(image, "png", target.toFile());

이 코드는 파일 크기를 제한하고 실제 이미지로 해석되는지 확인한다. 파일명은 서버에서 새로 만들며, 저장 경로는 웹 루트가 아닌 별도 디렉터리를 사용한다.

안전한 코드 한 번으로 모든 문제가 해결되는 것은 아니다. 이미지의 가로와 세로 크기, 사용자별 저장 용량, 업로드 횟수도 제한해야 한다. 중요한 서비스라면 파일을 즉시 공개하지 않고 격리 공간에 저장한 뒤 악성코드 검사를 거치는 절차가 필요하다.

업로드 파일은 웹 루트 밖에 저장해야 한다

웹쉘 업로드를 막는 가장 효과적인 설정은 업로드 파일을 웹 루트 밖에 저장하는 것이다. 웹 루트 외부에 있는 파일은 일반적으로 URL만으로 직접 호출하거나 실행할 수 없다.

사용자가 파일을 내려받아야 한다면 애플리케이션이 파일 식별자를 확인한 뒤 내용을 읽어 전달하도록 구현한다. 이때 요청한 사용자에게 해당 파일을 열람할 권한이 있는지도 검사해야 한다.

원본 파일명은 화면 표시를 위해 데이터베이스에 별도로 보관할 수 있지만, 서버의 실제 경로나 저장 파일명으로 사용해서는 안 된다.

업로드 디렉터리의 실행 권한을 제거해야 한다

서비스 구조상 파일을 웹 루트 내부에 저장해야 한다면 업로드 디렉터리에서 스크립트가 실행되지 않도록 웹 서버를 설정해야 한다.

Apache나 Nginx의 업로드 경로가 PHP, JSP, CGI 같은 실행 처리기와 연결되지 않도록 분리하고, 운영체제 권한에서도 불필요한 실행 권한을 제거해야 한다. 웹 서버 계정에는 업로드 파일을 저장하고 읽는 데 필요한 최소한의 권한만 부여하는 것이 좋다.

이 설정은 파일 검사가 예상하지 못한 방식으로 우회됐을 때 피해가 명령 실행으로 확대되는 것을 막아주는 중요한 방어선이다.

추가로 필요한 파일 업로드 보안 설정

파일 확장자는 차단 목록이 아닌 허용 목록을 기준으로 검사해야 한다. 이미지 기능이라면 JPG와 PNG, 문서 제출 기능이라면 PDF처럼 업무에 필요한 형식만 허용한다.

확장자뿐 아니라 서버에서 확인한 MIME 유형과 실제 파일 구조도 함께 검사해야 한다. 사용자가 전달한 Content-Type은 참고 정보로만 사용해야 한다.

파일 한 개의 최대 크기와 사용자별 업로드 횟수를 제한하면 대용량 파일이나 반복 요청으로 서버 자원이 소모되는 문제를 줄일 수 있다. 업로드 기능에는 사용자 인증과 CSRF 방어도 적용해야 한다.

파일을 브라우저에 제공할 때는 Content-Disposition: attachmentX-Content-Type-Options: nosniff를 적용하는 것이 좋다. 업로드 시각, 사용자 계정, 접속 IP, 원본 파일명과 검사 결과를 로그로 남기면 비정상적인 시도를 추적하는 데 도움이 된다.

파일 업로드 취약점 점검 항목

파일 업로드 기능을 점검할 때는 원본 파일명이 실제 저장 경로에 사용되는지 먼저 확인한다. 확장자 검사가 허용 목록으로 구성돼 있는지, 실제 파일 내용까지 검사하는지도 살펴봐야 한다.

업로드 파일의 저장 위치가 웹 루트 외부인지 확인하고, 웹 루트 안에 있다면 스크립트 실행이 차단돼 있는지 점검한다. 저장된 파일의 주소로 접근했을 때 서버 프로그램으로 실행되지 않고 단순한 파일로 처리돼야 한다.

파일 크기와 업로드 횟수 제한, 악성코드 검사, 사용자 인증, 접근 권한 확인 및 로그 기록도 함께 점검해야 한다. 소스코드에서는 getOriginalFilename(), transferTo(), Files.copy()와 같은 파일 처리 함수를 확인한 뒤 외부 입력값이 저장 경로에 영향을 주는지 추적하는 것이 중요하다.

자주 묻는 질문

확장자만 검사하면 웹쉘을 막을 수 있나요?

확장자 검사만으로는 부족하다. 서버 설정에 따라 처리 방식이 달라질 수 있고 파일명과 실제 내용도 다를 수 있다. 확장자, MIME 유형, 실제 파일 구조와 저장 위치를 함께 확인해야 한다.

업로드 폴더의 실행 권한만 제거하면 안전한가요?

웹쉘 실행 위험은 크게 줄어들지만 다른 위험은 남는다. 악성파일 배포나 저장 공간 고갈을 막으려면 파일 형식 검사, 크기 제한, 접근 권한과 악성코드 검사도 함께 적용해야 한다.

Content-Type이 image/jpeg이면 이미지로 판단해도 되나요?

그렇지 않다. Content-Type 값은 사용자가 변경할 수 있다. 서버의 이미지 처리 라이브러리를 이용해 실제 이미지로 해석되는지 확인해야 한다.

마무리

파일 업로드 취약점은 위험한 파일이 올라가는 문제와 해당 파일이 서버에서 실행되는 문제가 결합됐을 때 큰 피해로 이어진다. 따라서 확장자 몇 개를 차단하는 것만으로는 충분하지 않다.

필요한 파일 형식만 허용하고 실제 내용을 검사해야 한다. 원본 파일명 대신 서버에서 만든 이름을 사용하고, 업로드 파일은 웹 루트 밖에 저장하는 것이 안전하다. 웹 경로에 저장해야 한다면 해당 디렉터리에서 스크립트 실행을 반드시 차단해야 한다.

웹쉘 업로드를 막는 가장 중요한 원칙은 사용자가 올린 파일을 실행 가능한 프로그램으로 처리하지 않는 것이다. 파일 검증, 저장 경로 분리, 실행 권한 제거와 최소 권한 설정을 함께 적용해야 파일 업로드 취약점을 효과적으로 예방할 수 있다.

핵심 키워드: 파일 업로드 취약점, 웹쉘 업로드, 웹쉘 차단, 파일 업로드 보안, 확장자 검사, MIME 타입 검증, 업로드 디렉터리, 실행 권한 차단

댓글 남기기