λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°
개발 일지 πŸ‘©‍πŸ’»

같은 파일인데 νŠΉμ • μ»΄ν“¨ν„°μ—μ„œλ§Œ μ—…λ‘œλ“œ μ‹€νŒ¨? ν•œμ»΄μ˜€ν”ΌμŠ€ MIME νƒ€μž… 이슈 ν•΄κ²°

by chuyj15 2025. 11. 11.
728x90
λ°˜μ‘ν˜•
SMALL

πŸ€” 문제 상황

Spring Boot둜 λ§Œλ“  μ—‘μ…€ 파일 μ—…λ‘œλ“œ κΈ°λŠ₯μ—μ„œ μ΄μƒν•œ ν˜„μƒμ΄ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.

  • A 컴퓨터: 같은 μ—‘μ…€ 파일 μ—…λ‘œλ“œ β†’ βœ… 성곡
  • B 컴퓨터: 같은 μ—‘μ…€ 파일 μ—…λ‘œλ“œ β†’ ❌ "μ˜¬λ°”λ₯Έ μ—‘μ…€ 파일 ν˜•μ‹μ΄ μ•„λ‹™λ‹ˆλ‹€." 였λ₯˜

두 컴퓨터 λͺ¨λ‘:

  • λ™μΌν•œ Windows ν™˜κ²½
  • λ™μΌν•œ Chrome λΈŒλΌμš°μ €
  • λ™μΌν•œ .xlsx 파일

λŒ€μ²΄ μ™œ?! 🀯

πŸ” 원인 뢄석

범인은 λ°”λ‘œ... ν•œμ»΄μ˜€ν”ΌμŠ€!

B μ»΄ν“¨ν„°μ—λŠ” ν•œμ»΄μ˜€ν”ΌμŠ€(ν•œμ…€)κ°€ μ„€μΉ˜λ˜μ–΄ μžˆμ—ˆκ³ , .xlsx 파일 μ—°κ²° ν”„λ‘œκ·Έλž¨μ΄ ν•œμ…€λ‘œ μ„€μ •λ˜μ–΄ μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

둜그λ₯Ό ν™•μΈν•΄λ³΄λ‹ˆ:

  • A 컴퓨터: Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet βœ…
  • B 컴퓨터: Content-Type: application/haansoftxlsx ❌

μ™œ 이런 일이?

  1. ν•œμ»΄μ˜€ν”ΌμŠ€ μ„€μΉ˜ μ‹œ Windows λ ˆμ§€μŠ€νŠΈλ¦¬κ°€ 변경됨

    • 파일 ν™•μž₯μžλ³„ MIME νƒ€μž… 맀핑이 ν•œμ»΄ μ „μš©μœΌλ‘œ λ³€κ²½
    • .xlsx β†’ application/haansoftxlsx (ν•œμ»΄ μ „μš© MIME νƒ€μž…)
  2. λΈŒλΌμš°μ €λŠ” 운영체제의 MIME νƒ€μž…μ„ 따름

    • Chrome이 νŒŒμΌμ„ 읽을 λ•Œ OS의 λ ˆμ§€μŠ€νŠΈλ¦¬ 정보λ₯Ό μ°Έμ‘°
    • ν•œμ»΄μ˜€ν”ΌμŠ€κ°€ μ„€μΉ˜λ˜λ©΄ μžλ™μœΌλ‘œ ν•œμ»΄ MIME νƒ€μž… μ‚¬μš©
  3. μ„œλ²„λŠ” ν‘œμ€€ MIME νƒ€μž…λ§Œ ν—ˆμš©ν•˜λ„λ‘ 검증

    • ν•œμ»΄ μ „μš© MIME νƒ€μž…μ€ 검증 톡과 μ‹€νŒ¨ β†’ 였λ₯˜ λ°œμƒ
Windows λ ˆμ§€μŠ€νŠΈλ¦¬
└── HKEY_CLASSES_ROOT
    └── .xlsx
        └── Content Type: "application/haansoftxlsx"  // ν•œμ»΄μ˜€ν”ΌμŠ€κ°€ λ³€κ²½!

πŸ’‘ ν•΄κ²° 방법

1. μ„œλ²„ μΈ‘ ν•΄κ²° (ꢌμž₯ ✨)

κΈ°μ‘΄ μ½”λ“œ (문제 λ°œμƒ):

// MIME νƒ€μž… 검증
String contentType = file.getContentType();
if (contentType != null && 
    !contentType.equals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") &&
    !contentType.equals("application/vnd.ms-excel") &&
    !contentType.equals("application/octet-stream")) {
    result.setCode(PROGRAM_CODE.RESULT_FAIL);
    result.setMsg("μ˜¬λ°”λ₯Έ μ—‘μ…€ 파일 ν˜•μ‹μ΄ μ•„λ‹™λ‹ˆλ‹€.");
    return result;
}

κ°œμ„ λœ μ½”λ“œ:

// MIME νƒ€μž… 검증 (λ‹€μ–‘ν•œ ν™˜κ²½ 지원)
String contentType = file.getContentType();

if (contentType != null && 
    !contentType.equals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") &&  // ν‘œμ€€ .xlsx
    !contentType.equals("application/vnd.ms-excel") &&  // ν‘œμ€€ .xls
    !contentType.equals("application/octet-stream") &&  // 일반 λ°”μ΄λ„ˆλ¦¬
    !contentType.equals("application/haansoftxlsx") &&  // ν•œμ»΄μ˜€ν”ΌμŠ€ ν•œμ…€ ⭐
    !contentType.equals("application/x-tika-msoffice") &&  // Apache Tika
    !contentType.equals("application/zip")) {  // 일뢀 λΈŒλΌμš°μ €μ—μ„œ xlsxλ₯Ό zip으둜 인식
    result.setCode(PROGRAM_CODE.RESULT_FAIL);
    result.setMsg("μ˜¬λ°”λ₯Έ μ—‘μ…€ 파일 ν˜•μ‹μ΄ μ•„λ‹™λ‹ˆλ‹€. (Content-Type: " + contentType + ")");
    return result;
}

// 파일 ν™•μž₯자둜 μΆ”κ°€ 검증 (더 μ•ˆμ •μ )
String originalFilename = file.getOriginalFilename();
if (originalFilename != null) {
    String lowerFilename = originalFilename.toLowerCase();
    if (!lowerFilename.endsWith(".xlsx") && !lowerFilename.endsWith(".xls")) {
        result.setCode(PROGRAM_CODE.RESULT_FAIL);
        result.setMsg("μ—‘μ…€ 파일만 μ—…λ‘œλ“œ κ°€λŠ₯ν•©λ‹ˆλ‹€. (.xlsx, .xls)");
        return result;
    }
}

2. ν΄λΌμ΄μ–ΈνŠΈ μΈ‘ ν•΄κ²° (μž„μ‹œλ°©νŽΈ)

μ‚¬μš©μžκ°€ 직접 파일 μ—°κ²° ν”„λ‘œκ·Έλž¨μ„ λ³€κ²½:

  1. .xlsx 파일 우클릭 β†’ 속성
  2. μ—°κ²° ν”„λ‘œκ·Έλž¨ β†’ λ³€κ²½
  3. Microsoft Excel 선택
  4. 확인

단점: λͺ¨λ“  μ‚¬μš©μžμ—κ²Œ μš”κ΅¬ν•  수 μ—†μŒ (λΉ„ν˜„μ‹€μ )

πŸ“Š λ‹€μ–‘ν•œ ν™˜κ²½μ—μ„œμ˜ MIME νƒ€μž…

ν™˜κ²½ MIME Type
Microsoft Excel (ν‘œμ€€) application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
MS Excel (.xls) application/vnd.ms-excel
ν•œμ»΄μ˜€ν”ΌμŠ€ ν•œμ…€ application/haansoftxlsx ⭐
WPS Office application/wps-office.xlsx
일뢀 λΈŒλΌμš°μ € (zip 인식) application/zip
일반 λ°”μ΄λ„ˆλ¦¬ application/octet-stream
Apache Tika application/x-tika-msoffice

🎯 Best Practice

1. MIME νƒ€μž…λ§ŒμœΌλ‘œ κ²€μ¦ν•˜μ§€ λ§ˆμ„Έμš”

// ❌ λ‚˜μœ 예: MIME νƒ€μž…λ§Œ 검증
if (!contentType.equals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) {
    throw new InvalidFileException();
}

// βœ… 쒋은 예: MIME νƒ€μž… + 파일 ν™•μž₯자 + μ‹€μ œ λ‚΄μš© 검증
if (isInvalidMimeType(contentType)) {
    throw new InvalidFileException();
}
if (!filename.endsWith(".xlsx") && !filename.endsWith(".xls")) {
    throw new InvalidFileException();
}
// μ‹€μ œλ‘œ μ—‘μ…€ νŒŒμΌμΈμ§€ Apache POI둜 검증
try (Workbook workbook = WorkbookFactory.create(file.getInputStream())) {
    // μ •μƒμ μœΌλ‘œ 열리면 μ‹€μ œ μ—‘μ…€ 파일
}

2. ν—ˆμš© 리슀트 방식보닀 차단 리슀트 방식 κ³ λ €

// Option 1: ν—ˆμš© 리슀트 (ν˜„μž¬ 방식)
if (!isAllowedMimeType(contentType)) {
    throw new InvalidFileException();
}

// Option 2: 차단 리슀트 + 파일 λ‚΄μš© 검증 (더 μœ μ—°)
if (isDangerousMimeType(contentType)) {  // exe, sh, bat λ“±λ§Œ 차단
    throw new InvalidFileException();
}
// Apache POI둜 μ‹€μ œ μ—‘μ…€ νŒŒμΌμΈμ§€ 검증
validateExcelContent(file);

3. λ‘œκΉ… μΆ”κ°€λ‘œ 디버깅 μ‰½κ²Œ

log.info("파일 μ—…λ‘œλ“œ μ‹œλ„ - 파일λͺ…: {}, Content-Type: {}, 크기: {} bytes", 
    filename, contentType, file.getSize());

πŸš€ μΆ”κ°€ 고렀사항

λ³΄μ•ˆμ„ μœ„ν•œ μ‹€μ œ 파일 λ‚΄μš© 검증

MIME νƒ€μž…κ³Ό ν™•μž₯μžλŠ” μ‰½κ²Œ μ‘°μž‘ κ°€λŠ₯ν•©λ‹ˆλ‹€. μ§„μ§œ λ³΄μ•ˆμ΄ ν•„μš”ν•˜λ‹€λ©΄:

// Apache POI둜 μ‹€μ œ μ—‘μ…€ νŒŒμΌμΈμ§€ 검증
try (InputStream is = file.getInputStream();
     Workbook workbook = WorkbookFactory.create(is)) {
    // μ •μƒμ μœΌλ‘œ Workbook이 μƒμ„±λ˜λ©΄ μ‹€μ œ μ—‘μ…€ 파일
    log.info("μ—‘μ…€ 파일 검증 성곡: {} μ‹œνŠΈ 발견", workbook.getNumberOfSheets());
} catch (Exception e) {
    throw new InvalidFileException("μ—‘μ…€ 파일이 μ†μƒλ˜μ—ˆκ±°λ‚˜ μ˜¬λ°”λ₯Έ ν˜•μ‹μ΄ μ•„λ‹™λ‹ˆλ‹€.");
}

μ˜μ‘΄μ„± μΆ”κ°€

<!-- pom.xml -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.3</version>
</dependency>

πŸ“ 정리

  1. λΈŒλΌμš°μ €μ˜ Content-Type은 OS의 파일 μ—°κ²° ν”„λ‘œκ·Έλž¨μ— 따라 λ‹¬λΌμ§‘λ‹ˆλ‹€
  2. ν•œμ»΄μ˜€ν”ΌμŠ€ μ„€μΉ˜λ§ŒμœΌλ‘œλ„ MIME νƒ€μž…μ΄ λ³€κ²½λ©λ‹ˆλ‹€
  3. μ„œλ²„λŠ” λ‹€μ–‘ν•œ ν™˜κ²½μ˜ MIME νƒ€μž…μ„ ν—ˆμš©ν•΄μ•Ό ν•©λ‹ˆλ‹€
  4. MIME νƒ€μž… 검증 + ν™•μž₯자 검증 + μ‹€μ œ λ‚΄μš© 검증을 ν•¨κ»˜ μ‚¬μš©ν•˜μ„Έμš”

같은 파일, 같은 λΈŒλΌμš°μ €μΈλ° λ‹€λ₯Έ κ²°κ³Όκ°€ λ‚˜μ˜¨λ‹€λ©΄... μ„€μΉ˜λœ ν”„λ‘œκ·Έλž¨μ„ μ˜μ‹¬ν•΄λ³΄μ„Έμš”! πŸ•΅οΈ


κ΄€λ ¨ ν‚€μ›Œλ“œ: Spring Boot, 파일 μ—…λ‘œλ“œ, MIME Type, Content-Type, ν•œμ»΄μ˜€ν”ΌμŠ€, Hancom Office, Excel, MultipartFile, 파일 검증

참고 자료:

728x90
λ°˜μ‘ν˜•
LIST