업로드 파일 형식 검증 누락

업로드 파일 형식 검증 누락

설명

파일 업로드 엔드포인트에서 확장자, 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 시그니처만으로 전체 파일의 유효성이나 무해함을 보장할 수는 없습니다. 필요한 경우 이미지 디코딩·재인코딩 등 용도에 맞는 콘텐츠 검증을 추가하세요.

참조