소프트웨어 개발보안 가이드 분석(2021) : 위험한 형식 파일 업로드
서버 측에서 실행될 수 있는 스크립트 파일(asp, jsp, php 파일 등)이 업로드 가능하고, 이 파일을
공격자가 웹으로 직접 실행시킬 수 있는 경우, 시스템 내부명령어를 실행하거나 외부와 연결하여
시스템을 제어할 수 있는 보안약점.
소프트웨어 개발보안가이드(2021), 한국인터넷진흥원
웹 애플리케이션에서 파일 업로드는 사용자가 컨텐츠를 공유하고 풍부한 인터랙션을 경험할 수 있게 하는 필수적인 기능입니다. 하지만, 이 편리함의 이면에는 심각한 보안 위험이 도사리고 있습니다. 특히, 서버 측에서 실행될 수 있는 스크립트 파일(예: ASP, JSP, PHP 파일 등)의 무단 업로드와 그 후의 직접 실행은 웹 애플리케이션의 보안에 치명적인 약점이 될 수 있습니다.
이번 포스팅에서는 이러한 악성코드 업로드 방식의 공격에 대응하기 위한 방법들을 심도 있게 탐구하고자 합니다. 파일 업로드 기능을 안전하게 구현하는 방법, 그리고 최신 보안 표준과 관행을 적용하는 방법 등을 자세히 살펴보며, 웹 애플리케이션의 보안을 강화하는 데 필요한 실질적인 조치들을 제시해보려고 합니다.
"위험한 형식 파일 업로드"의 이해
"위험한 형식 파일 업로드" 공격은 단순히 비인가된 파일이 서버에 업로드되는 것을 넘어서, 공격자가 이 파일들을 웹을 통해 직접 실행할 수 있게 되어, 서버의 내부 명령을 실행하거나 외부 시스템과의 연결을 통해 시스템 전체를 제어할 수 있는 권한을 부여받을 수 있다는 점에서 그 위험성이 배가 됩니다. 이는 공격자에게 시스템의 문을 활짝 열어주는 것과 다름없으며, 결과적으로 데이터 유출, 서비스 거부(DoS) 공격, 추가적인 악성코드 배포 등 다양한 보안 사고로 이어질 수 있습니다.
해당 보안약점을 이해하는 데 있어서 주목해야 하는 점은, 업로드된 파일이 웹 서버에서 직접 실행되는 것을 방지하는 것은 단순히 비인가 파일의 업로드를 막는 것을 넘어서는 문제라는 것입니다. 예를 들어 공격자로 인해 웹쉘의 업로드 시도가 성공했다 하더라도 해당 웹셀 스크립트를 실행하지 못한다면 공격자 입장에서는 큰 의미가 없을 수 있기 때문입니다.
이러한 이유로 소프트웨어 개발보안 가이드에서는 단순히 업로드되는 파일에 대한 확장자 검증 뿐만 아니라 그 외에 다양한 방법의 대응 방안을 제시하고 있습니다.
안전한 업로드 구현 방법
앞서 설명드린 바와 같이 이 취약점의 시작은 주로 웹 애플리케이션에서 업로드된 파일에 대한 충분한 검증이 이루어지지 않았을 때 발생합니다. 파일의 확장자나 MIME 타입 등을 검사하지 않고, 사용자가 업로드한 파일을 그대로 서버에 저장할 경우, 공격자는 서버에 악의적인 코드가 포함된 파일을 업로드할 기회를 갖게 되죠. 이후 업로드된 파일을 웹을 통해 실행하게 될 수 있기 때문에 업로드 뿐만아니라 실행 되지 못하도록 하는 것까지 감안하여 시큐어코딩을 적용해야 합니다.
일반적으로 알려진 대응방안은 다음과 같습니다.
- 화이트리스트 방식 적용 : 업로드를 허용하는 파일의 확장자를 명시적으로 정의하고, 그 외의 확장자를 가진 파일은 업로드를 거부합니다.
- 파일명 변경 : 업로드된 파일의 이름을 서버에서 임의로 변경하여 외부에서 접근하기 어렵게 합니다.
- 서버 측 파일 실행 금지 : 업로드된 파일이 저장되는 디렉토리에서 스크립트 실행을 금지하여, 심지어 업로드된 파일이 실행 가능한 파일이라 하더라도 실행되지 않도록 합니다.
- 애플리케이션 레벨에서의 검증 : 클라이언트 측 검증 뿐만 아니라 서버 측에서도 파일의 확장자, 크기, MIME 타입 등을 철저히 검증합니다.
// 파일 확장자 검사
String fileName = uploadedFile.getName();
String fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1);
List<String> allowedExtensions = Arrays.asList("jpg", "jpeg", "png", "gif");
if (!allowedExtensions.contains(fileExtension.toLowerCase())) {
throw new SecurityException("허용되지 않은 파일 형식입니다.");
}
// 파일명 변경 및 안전한 디렉토리에 저장
String safeFileName = UUID.randomUUID().toString() + "." + fileExtension;
File destination = new File(safeDirectory, safeFileName);
위의 Java 코드는 웹 애플리케이션에서 파일 업로드 기능을 구현할 때, 보안을 강화하기 위한 좋은 예시를 제공합니다. 이 코드는 크게 두 부분으로 나눌 수 있으며, 각각의 목적은 다음과 같습니다.
- 파일 확장자 검사
이 부분의 코드는 업로드된 파일의 확장자를 검사하여, 애플리케이션에서 정의한 허용된 파일 확장자 목록에 포함되어 있는지 확인합니다. 이는 악의적인 파일, 특히 실행 가능한 스크립트 파일(예: .php, .asp, .jsp 등)이 시스템에 업로드되는 것을 방지하는 데 목적이 있습니다. 확장자 검사를 통해 웹셸 공격과 같은 보안 위협으로부터 시스템을 보호할 수 있습니다.
- 파일명 변경 및 안전한 디렉토리에 저장
업로드된 파일의 이름을 변경하는 것은 또 다른 중요한 보안 조치입니다. 이는 두 가지 목적을 가집니다: 첫째, 공격자가 업로드한 파일을 추측하여 직접 접근하는 것을 방지하고, 둘째, 업로드된 파일이 겹치지 않도록 하여 기존 파일을 덮어쓰는 것을 방지합니다. 파일을 안전한 디렉토리에 저장하는 것도 중요한데, 이는 업로드된 파일이 웹 서버에서 직접 실행되지 않도록 하기 위함입니다.
'org.apache.commons.io.FilenameUtils'를 활용한 예제
org.apache.commons.io.FilenameUtils는 Apache Commons IO 라이브러리에 포함된 유틸리티 클래스로, 파일 이름, 파일 경로, 확장자 등과 관련된 다양한 작업을 쉽게 처리할 수 있는 메소드들을 제공합니다. 이 클래스는 파일 시스템 작업에서 자주 발생하는 문제들을 해결하기 위해 설계되었습니다. FilenameUtils는 크로스 플랫폼 호환성을 고려하여 UNIX 및 Windows 경로 규칙을 모두 지원합니다.
주요 구성 및 기능은 다음과 같습니다.
구분 | 메서드 | 기능 |
경로 분석 및 조작 | getExtension(String filename) | 파일의 확장자를 반환합니다. |
getName(String filename) | 경로의 마지막 부분에 해당하는 파일명을 반환합니다. | |
getBaseName(String filename) | 확장자를 제외한 파일명을 반환합니다. | |
getPath(String filename) | 전체 경로 중 파일명을 제외한 경로를 반환합니다. | |
경로 비교 및 검증 | equalsNormalizedOnSystem(String filename1, String filename2) | 두 파일 경로를 시스템에 맞게 정규화한 후 비교합니다. |
isExtension(String filename, String extension) | 파일명의 확장자가 주어진 확장자와 일치하는지 검사합니다. | |
경로 구성 | concat(String basePath, String fullFilenameToAdd) | 기본 경로에 파일 경로를 추가합니다. |
normalize(String filename) | 입력된 파일 경로를 정규화하여 반환합니다. |
코드 예제
import org.apache.commons.io.FilenameUtils;
// 파일 업로드 처리 함수
public void handleFileUpload(FileItem fileItem) throws Exception {
// 허용된 파일 확장자 리스트
List<String> allowedExtensions = Arrays.asList("jpg", "jpeg", "png", "gif");
// 파일 확장자 추출
String fileName = fileItem.getName();
String fileExtension = FilenameUtils.getExtension(fileName).toLowerCase();
// 확장자 검증
if (!allowedExtensions.contains(fileExtension)) {
throw new Exception("허용되지 않은 파일 형식입니다.");
}
// 파일 내용 검사 (예시로, 텍스트 파일 내 악의적인 PHP 코드 검사)
if ("txt".equals(fileExtension)) {
String fileContent = fileItem.getString();
if (fileContent.contains("<?php")) {
throw new Exception("악의적인 스크립트가 포함된 파일은 업로드할 수 없습니다.");
}
}
// 파일 저장 경로 설정 (실행 권한이 없는 안전한 디렉토리를 사용)
File uploadDir = new File("/path/to/safe/dir");
if (!uploadDir.exists()) {
uploadDir.mkdirs();
}
// 파일 저장
File uploadedFile = new File(uploadDir, fileName);
fileItem.write(uploadedFile);
// 업로드된 파일에 대한 정보 로깅
log.info("File uploaded: " + uploadedFile.getAbsolutePath());
}
이 코드는 Java를 사용한 파일 업로드 처리의 한 예시로, 특정 파일 확장자의 업로드만을 허용하고, 업로드된 파일의 내용을 검사하여 악의적인 코드의 존재 여부를 확인하는 과정을 포함하고 있습니다. 이 과정은 웹 애플리케이션에서 파일 업로드 기능을 제공할 때 발생할 수 있는 보안 위험을 최소화하기 위한 중요한 단계입니다. 아래는 코드의 주요 부분에 대한 설명입니다.
1) 파일 확장자 검증
- FilenameUtils.getExtension(fileName) 메소드를 사용하여 업로드된 파일의 확장자를 추출하고, 이를 소문자로 변환하여 fileExtension 변수에 저장합니다.
- 정의된 allowedExtensions 리스트에 fileExtension이 포함되어 있는지 검사하여, 허용되지 않은 파일 형식인 경우 예외를 발생시킵니다.
2) 파일 내용 검사
- 파일 확장자가 "txt"인 경우, 파일의 내용을 읽어 특정 패턴(여기서는 PHP 스크립트 시작 태그 <?php)이 포함되어 있는지 검사합니다. 이러한 패턴이 발견되면 업로드를 거부하고 예외를 발생시킵니다.
3) 파일 저장
- 안전한 디렉토리("/path/to/safe/dir")를 지정하고, 해당 디렉토리가 존재하지 않는 경우 새로 생성합니다.
- File 객체를 사용하여 지정된 디렉토리에 파일을 저장합니다. 이 때, 원본 파일 이름(fileName)을 그대로 사용합니다.
4) 로깅
- 업로드된 파일의 절대 경로 정보를 로깅합니다. 이는 추후 문제 발생 시 추적을 용이하게 하기 위함입니다.