설명
파일 업로드 엔드포인트에서 확장자, MIME 타입, 매직 바이트 같은 allowlist 검증 없이 업로드 파일을 수락하면 악성 파일이 서버에 저장될 수 있습니다.
잠재적 영향
- 실행 가능한 파일이나 스크립트가 서버 파일 시스템에 저장될 수 있습니다.
- 정적 제공 경로에 저장된 파일이 사용자에게 악성 콘텐츠로 제공될 수 있습니다.
해결 방법
- 확장자, MIME 타입, 매직 바이트를 allowlist로 검증하세요.
- 저장 파일명은 서버에서 생성하고 업로드 디렉터리는 실행 경로와 분리하세요.
multer의fileFilter와 별도의 콘텐츠 검증 계층을 함께 사용하세요.
예시
Express의 app과 격리 디렉터리는 미리 준비하는 전제입니다. 변경 전의 upload에는 파일 형식 검증이 없다고 가정합니다.
변경 전
javascript
app.post("/upload", upload.single("file"), (req, res) => {
res.json({ path: req.file.path });
});
변경 후
javascript
const crypto = require("crypto");
const fs = require("fs/promises");
const path = require("path");
const multer = require("multer");
// 웹 공개/실행 경로가 아니며 다른 사용자가 쓸 수 없는 격리 디렉터리
const QUARANTINE_DIR = "/srv/app/upload-quarantine";
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const upload = multer({
storage: multer.diskStorage({
destination: QUARANTINE_DIR,
filename(req, file, cb) {
cb(null, `${crypto.randomUUID()}.png`);
},
}),
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter(req, file, cb) {
const extension = path.extname(file.originalname).toLowerCase();
const allowed = file.mimetype === "image/png" && extension === ".png";
cb(null, allowed);
},
});
async function hasPngSignature(filePath) {
const handle = await fs.open(filePath, "r");
try {
const header = Buffer.alloc(PNG_SIGNATURE.length);
const { bytesRead } = await handle.read(header, 0, header.length, 0);
return bytesRead === header.length && header.equals(PNG_SIGNATURE);
} finally {
await handle.close();
}
}
app.post("/upload", upload.single("file"), async (req, res, next) => {
if (!req.file) return res.status(400).send("invalid file type");
try {
if (!(await hasPngSignature(req.file.path))) {
await fs.unlink(req.file.path);
return res.status(400).send("invalid file type");
}
return res.json({ id: req.file.filename });
} catch (error) {
await fs.unlink(req.file.path).catch(() => {});
return next(error);
}
});
설명:
- 변경 전: Express 파일 업로드 엔드포인트에서 확장자, MIME 타입, 매직 바이트 같은 allowlist 검증 없이 업로드 파일을 수락하면 악성 스크립트나 실행 가능한 파일이 서버에 저장될 수 있습니다.
- 변경 후: 크기, 확장자, MIME 타입, PNG 시그니처를 allowlist로 검증하고 서버가 생성한 이름으로 비공개 격리 디렉터리에 저장합니다. 필터에서 제외된 파일은
req.file부재를 처리하고, 저장 후 콘텐츠 검증에 실패하거나 오류가 발생하면 임시 파일을 삭제합니다. PNG 시그니처만으로 전체 파일의 유효성이나 무해함을 보장할 수는 없습니다. 필요한 경우 이미지 디코딩·재인코딩 등 용도에 맞는 콘텐츠 검증을 추가하세요.