説明
アップロード形式と、その後の保存・処理方法を十分に制限しないと、ファイルがサーバーや利用者の環境で実行されたり、危険なコンテンツとして解釈されたりする可能性があります。
想定される影響
JSP、ASP、PHPなどとしてサーバーやアプリケーションが解釈すると、アップロードファイルからコードが実行される可能性があります。ファイルシステムでそのファイルに実行権限を与えていなくても、インタープリターが読み取って実行する場合があります。
対処方法
- 元のファイル名やクライアントのMIMEタイプを信頼せず、サーバーで名前を生成し、サイズと実際の内容を検証してください。
- Webルート外に保存し、書き込み権限を制限してください。アップロードをサーバーコードとして実行しないよう、Webサーバーや後続の処理を構成します。ディレクトリの検索権限と、ファイルの実行権限は区別してください。
- 正規化した保存先をアップロードルートと構成要素単位で比較し、新規ファイルだけをアトミックに作成してください。
- 後続処理が必要なら、マルウェア検査と隔離された処理環境を使ってください。
例
変更前
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");
}
// Webルート外にUPLOAD_ROOTを作成し、アップロードをコードとして実行させないでください。
Files.write(target, content, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
return "uploadSuccess";
}
}
変更前は利用者が指定した名前で保存し、意図しないパスへの書き込みや、後でスクリプトとして解釈される危険があります。変更後はUUIDを基に.txt名を作り、サイズとUTF-8の実際のバイト列を検証します。保存先を確認したうえでCREATE_NEWを使い、新規ファイルだけを保存します。
関連CVE
- CVE-2023-5227: PHP製FAQアプリケーションのアップロードMIMEタイプ検証不足
- CVE-2001-0901: WebメールがSSIを含む.shtml添付ファイルを保存
- CVE-2002-1841: PHPアップロードのファイル形式制限不足