설명
애플리케이션이 업로드 형식과 저장·처리 방식을 충분히 제한하지 않으면, 업로드된 파일이 서버나 사용자 환경에서 실행되거나 위험한 콘텐츠로 해석될 수 있습니다.
잠재적 영향
- Execute Unauthorized Code or Commands (무단 코드 실행): 업로드된 파일이 코드로 해석되고 실행되는 경우, 웹 서버 확장자(.jsp, .asp, .php 등)로 인해 임의의 코드 실행이 발생할 수 있습니다. 이는 파일 시스템 권한이 실행을 명시적으로 허용하지 않더라도, 웹 서버가 JAVA, PHP등과 같은 프로그램을 자동으로 실행할 수 있기 때문입니다.
해결 방법
- 원본 파일명과 클라이언트가 보낸 MIME 타입을 신뢰하지 말고, 서버가 파일명을 생성하며 크기와 실제 콘텐츠 형식을 검증합니다.
- 업로드 디렉터리는 웹 루트 밖에 두고 쓰기 권한을 제한하세요. 업로드 파일을 서버 코드로 실행하지 않도록 웹 서버와 후속 처리기를 구성하세요. 디렉터리에 필요한 검색 권한과 파일 실행 권한은 구분해야 합니다.
- 정규화된 대상 경로가 업로드 루트 아래에 있는지 구성 요소 단위로 확인하고, 새 파일만 원자적으로 생성합니다.
- 업로드된 파일을 추가 처리해야 한다면 악성 코드 검사와 격리된 처리 환경을 사용합니다.
예시
변경 전
java
@PostMapping("/upload")
public String handleFileUpload(@RequestParam("file") MultipartFile file) throws IOException {
// 파일을 서버에 저장하는 위험한 코드
String fileName = file.getOriginalFilename();
File destinationFile = new File("/uploads/" + fileName);
file.transferTo(destinationFile);
return "uploadSuccess";
}
변경 후
다음 예시는 UTF-8 텍스트만 저장하는 Java 11 이상 발췌입니다. 관리자만 업로드 루트와 상위 경로를 변경할 수 있어야 하며, Spring의 multipart 요청 크기도 제한하세요. 텍스트 형식 검증은 악성 코드 검사가 아니므로 저장한 내용을 실행하거나 HTML로 그대로 렌더링하지 마세요.
java
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.UUID;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
public final class SafeUploadController {
private static final Path UPLOAD_ROOT =
Path.of("/var/lib/myapp/uploads").toAbsolutePath().normalize();
private static final long MAX_TEXT_SIZE = 1024 * 1024;
@PostMapping("/upload")
public String handleFileUpload(@RequestParam("file") MultipartFile file) throws IOException {
if (file.isEmpty() || file.getSize() > MAX_TEXT_SIZE) {
throw new IOException("Invalid file size");
}
byte[] content = file.getBytes();
String text;
try {
text = StandardCharsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(ByteBuffer.wrap(content))
.toString();
} catch (CharacterCodingException e) {
throw new IOException("The upload is not valid UTF-8 text", e);
}
if (text.codePoints().anyMatch(c -> c == 0
|| (Character.isISOControl(c) && c != '\n' && c != '\r' && c != '\t'))) {
throw new IOException("Invalid text content");
}
String storedName = UUID.randomUUID() + ".txt";
Path target = UPLOAD_ROOT.resolve(storedName).normalize();
if (!target.startsWith(UPLOAD_ROOT)) {
throw new IOException("Invalid upload destination");
}
// UPLOAD_ROOT를 웹 루트 밖에 미리 만들고, 업로드 파일의 코드 실행을 막습니다.
Files.write(target, content, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
return "uploadSuccess";
}
}
설명:
- 변경 전: 위험한 코드에서는 업로드된 파일의 원본 이름을 그대로 사용하여 저장하기 때문에, web shell과 같은 악성 스크립트가 업로드되어 서버에서 실행될 위험이 있습니다.
- 변경 후: 원본 파일명과 확장자를 사용하지 않고 UUID 기반 서버 파일명을 생성합니다. 크기를 제한하고 실제 바이트가 허용된 UTF-8 텍스트인지 검증하며, 정규화된 대상이 웹 루트 밖의 업로드 루트 아래인지 확인한 후
CREATE_NEW로 새 파일만 저장합니다.
관련 CVE
- CVE-2023-5227: PHP-based FAQ management app does not check the MIME type for uploaded images
- CVE-2001-0901: Web-based mail product stores ".shtml" attachments that could contain SSI
- CVE-2002-1841: PHP upload does not restrict file types