Skip to content

[파일보안] 첨부파일 검증·스캔·저장소 분리 #49

Description

@RosieOh

🧑‍💻 오늘의 개발하면서 생긴 크고 작은 이슈사항을 기록해주세요

💚 어떤 기능인가요?

[ File Security ] 첨부파일 업로드 보안을 강화하고 저장소를 분리(S3 등)합니다.
MIME/크기 검증과 스캔 체계를 적용해 악성 파일 유입 리스크를 낮춥니다.

✅ To Dos

  • 업로드 허용 MIME/확장자/최대 크기 정책 정의
  • 서버측 파일 시그니처 검증 추가
  • 바이러스 스캔 연동 지점 설계
  • 외부 저장소(S3) + 서명 URL 접근 구조 구현
  • 파일 접근 권한/만료 정책 반영
  • 실패 케이스/보안 테스트 추가

Activity

  1. added theissue type on Aug 4, 2026
  2. RosieOh commented on Aug 4, 2026

    @RosieOh
    ContributorAuthor

    부분 완료 — 남은 범위만 유지합니다

    #57 에서 완료

    • 확장자·MIME·크기 검증
    • 경로 조작(path traversal) 차단
    • FileStorageService 추상화 — S3 로 교체 가능한 구조

    남은 범위 (이 이슈에서 계속)

    • 바이러스/악성코드 스캔
    • 저장소 실제 분리 (현재는 로컬 구현체, 추상화만 되어 있음)
    • 서명 URL 등 접근 통제
  3. RosieOh commented on Aug 10, 2026

    @RosieOh
    ContributorAuthor

    부분 완료 상태입니다. 검증은 되어 있고, 스캔과 저장소 분리가 남았습니다.

    이미 되어 있는 것 (LocalFileStorageService):

    • 확장자 허용 목록 (ALLOWED_EXTENSIONS)
    • Content-Type 허용 목록 (ALLOWED_CONTENT_TYPES)
    • 크기 제한 (app.storage.max-file-size-bytes, 기본 10MB)
    • 경로 조작 차단 — 디렉터리 이름에서 구분자·상위 이동 제거

    남은 것:

    • 악성코드 스캔 — ClamAV 같은 외부 스캐너 연동이 필요합니다. 자바 안에서 할 수 있는 일이 아닙니다.
    • 저장소 분리 — 지금은 애플리케이션과 같은 디스크에 저장합니다. S3 등으로 분리하려면 인프라 결정이 선행돼야 합니다.

    둘 다 코드보다 인프라·비용 결정이 먼저입니다. 그래서 열어 둡니다.

    한 가지 덧붙이면, 확장자·MIME 허용 목록은 파일 내용을 보지 않습니다. .jpg 로 이름 붙인 실행 파일은 통과합니다. 매직 넘버 검사는 스캐너 연동과 같이 하는 편이 낫습니다.

  4. RosieOh commented on Oct 23, 2026

    @RosieOh
    ContributorAuthor

    체크리스트를 코드와 대조했습니다 — 6개 중 3개는 이미 되어 있습니다

    항목 상태 어디
    허용 MIME/확장자/최대 크기 정책 ✅ LocalFileStorageService.ALLOWED_EXTENSIONS(jpg·jpeg·png·gif·webp·heic·pdf), ALLOWED_CONTENT_TYPES, app.storage.max-file-size-bytes(기본 10MB)
    서버측 파일 시그니처 검증 ✅ FileSignatureValidator + verifySignature(). 확장자와 내용이 다르면 거부
    바이러스 스캔 연동 지점 ✅ FileScanner 인터페이스, ClamAvFileScanner(clamd), NoOpFileScanner. app.storage.scan.enabled 로 켠다. 켜면 검사기에 닿지 못할 때 업로드를 받지 않는다(503)
    파일 접근 권한/만료 정책 ⚠️ 부분 소유권 확인 다운로드 경로는 있다. 정적 경로가 우회였다 — 아래
    외부 저장소(S3) + 서명 URL ❌ LocalFileStorageService 만 있다. 결정이 필요하다 — 아래
    실패 케이스/보안 테스트 ✅ HealthRecordAttachmentServiceTest(소유권·교차 접근·404 동형), 이번에 삭제·정적 서빙 추가

    대조하면서 나온 것 둘 (#152)

    1. 첨부를 지워도 저장된 파일이 남았다

    FileStorageService.delete(key) 는 있었지만 부르는 곳이 없었습니다. 첨부 개별 삭제와 건강기록 삭제(cascade 는 행만 지운다) 양쪽에서 파일이 디스크에 남았습니다. 진단서·처방전이 "지웠다" 고 생각한 뒤에도 서버에 남는 상태였습니다.

    2. 로그인한 아무나 정적 경로로 남의 첨부를 받을 수 있었다

    업로드 루트 전체가 /files/** 로 정적 서빙되고 있었습니다. 보안 설정이 프로필 이미지만 공개로 두고 나머지는 인증을 요구했지만, 인증을 거치는 다운로드 경로는 소유권까지 확인하는 반면 정적 경로는 로그인만 보면 통과시켰습니다. 주소를 알게 된 다른 회원이 남의 진단서를 받아갈 수 있었습니다.

    정적 서빙을 profile-images 하위로만 좁혔습니다.

    남은 것 — S3 전환은 결정이 필요합니다

    지금 상태로도 보안 항목은 대체로 충족합니다(확장자·시그니처·크기·스캔 지점·소유권). S3 가 주는 것은 다른 축입니다.

    S3 로 얻는 것 지금 상태의 문제
    앱 컨테이너를 교체해도 파일이 유지된다 배포할 때마다 ./uploads 가 컨테이너와 함께 사라진다 (볼륨을 붙이지 않으면)
    서명 URL 로 만료가 생긴다 다운로드가 앱을 거치므로 앱 메모리·대역을 쓴다
    디스크 용량이 인스턴스에 묶이지 않는다 1GB 프리티어 인스턴스에 쌓인다

    첫 줄이 당장 문제입니다. 운영 배포가 시작되면(#94) 첨부가 배포마다 날아갑니다. 지금 docker run 에 볼륨 마운트가 있는지 확인하고, 없으면 S3 전환 전에 볼륨이라도 붙여야 합니다.

    어느 쪽으로 갈지 알려주시면 이어가겠습니다.

    • A. 볼륨 먼저 — docker run -v /opt/carecode/uploads:/app/uploads. 작고 즉시 가능
    • B. S3 전환 — FileStorageService 구현체 추가 + 서명 URL. 자격증명·버킷 정책 결정 필요
  5. RosieOh commented on Oct 24, 2026

    @RosieOh
    ContributorAuthor

    현재 상태 — 6개 중 5개 완료

    To Do 상태 어디에
    업로드 허용 MIME/확장자/최대 크기 정책 정의 완료 core/storage/UploadPolicy
    서버측 파일 시그니처 검증 추가 완료 core/storage/FileSignatureValidator
    바이러스 스캔 연동 지점 설계 완료 FileScanner + ClamAvFileScanner / NoOpFileScanner
    외부 저장소(S3) + 서명 URL 접근 구조 구현 부분 저장소는 #155 로 완료, 서명 URL 은 의도적으로 넣지 않음
    파일 접근 권한/만료 정책 반영 부분 권한 완료, 만료 없음
    실패 케이스/보안 테스트 추가 완료 S3FileStorageServiceTest 9개 + 기존 업로드 테스트

    검증은 전부 UploadPolicy 로 모았다. 저장소 구현을 새로 추가해도 검사를 건너뛸 수 없다 —
    LocalFileStorageService 와 S3FileStorageService 가 같은 정책을 거친다.

    서명 URL 을 넣지 않은 이유

    건강기록 첨부는 민감정보다. 서명 URL 은 주소를 가진 사람이면 누구나 만료 전까지 열 수 있고,
    그 주소는 로그·히스토리·공유로 새어 나간다. 대신 소유권을 확인하는 애플리케이션 경로를 거친다:

    GET /health/records/{recordId}/attachments/{id}/download
    

    S3FileStorageService.store() 가 돌려주는 url 도 공개 주소가 아니라 저장 키다.
    정적 서빙은 프로필 이미지(/files/profile-images/**)로만 좁혀 뒀고, 업로드 루트 전체는 닫혀 있다.

    이 선택은 대역폭을 앱이 쓴다는 비용이 있다. 첨부가 커지면 서명 URL + 짧은 만료로 바꿀 수 있지만,
    그때도 발급 시점에 소유권을 확인해야 한다. 지금 구조가 그 확인을 이미 갖고 있다.

    남은 것

    남은 두 항목은 "지금 해야 할 일" 이 아니라 선택지다. 더 할 일이 생기면 그때 범위를 다시 적는다.

  6. added this to the Production Readiness milestone on Oct 24, 2026
  7. RosieOh commented on Oct 24, 2026

    @RosieOh
    ContributorAuthor

    라벨 정정: 가이드상 type:security 에는 P2 를 붙이지 않는다(보안 결함은 P0/P1). 이 이슈가 다루던 보안 결함은 해소됐고 남은 두 항목(서명 URL·만료 정책)은 결함이 아니라 선택지이므로 type:feature + priority:P2 로 둔다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions