확인한 것만 배포한다: SBOM, CI 고정, 이미지 서명, 역직렬화

아프로·2026년 11월 2일

확인한 것만 배포한다 표지

롯데카드 결제 서버에는 2017년에 나온 WebLogic 패치가 2025년까지 빠져 있었습니다. 2022년 6월 이후 거래가 끊긴 서버였고 보안관제에서도 빠져 있었는데, 그 서버를 거쳐 297만 명의 정보가 빠져나갔습니다(롯데카드 297만 명). 2026년 3월에는 EU 집행위원회 CI에서 실행된 오염된 보안 스캐너가 클라우드 키를 밖으로 보냈고, 압축 91.7GB가 공개됐습니다(CERT-EU, EU 집행위원회). 두 사고에는 실행 중인 서버와 도구가 무엇인지 확인하는 단계가 빠져 있었다는 공통점이 있습니다.

이 시리즈의 공격을 실제 쇼핑몰 코드에서 직접 재연하고 막아 보는 인프런 강의가 열렸습니다. 실제 해킹 사고로 배우는 Spring Boot·Express 시큐어 코딩 · 이 글과 이어지는 수업: 7-1 ~ 7-7

한눈에 보기

의존성에서 운영까지 네 개의 확인 지점. 빌드 때 SBOM과 KEV 대조, CI에서 커밋 해시 고정과 단기 자격증명, 배포 전 이미지 서명 확인, 실행 중에는 형식을 고정한 역직렬화

위험어디서 생기나막는 법 한 줄OWASP
알려진 취약 부품오래된 의존성, 목록에서 빠진 서버SBOM을 만들고 악용 중 취약점(KEV) 목록과 자동 대조A03
바뀔 수 있는 CI 도구와 장기 키버전 태그로 받은 외부 Action, CI에 둔 클라우드 키전체 커밋 해시로 고정, 실행할 때만 받는 단기 자격증명A03
확인 없이 배포되는 이미지이름과 태그만 보고 받는 배포빌드 직후 서명, 배포 스크립트에서 서명 확인A08
신뢰할 수 없는 데이터 역직렬화요청 본문을 Java 객체로 그대로 복원형식을 고정한 DTO, 허용 타입 필터A08

1. SBOM과 악용 중 취약점(KEV) 대조

무엇이 문제인가

서비스 하나에는 직접 쓴 코드보다 가져다 쓴 부품이 훨씬 많습니다. 어떤 부품의 어떤 버전이 들어 있는지 모르면, 취약점 공지가 나와도 우리 서비스가 해당되는지 판단할 수 없습니다. 목록에 없는 서버와 부품은 아무도 패치하지 않습니다.

SBOM(Software Bill of Materials)은 서비스에 들어간 부품과 버전을 적은 목록입니다. KEV(Known Exploited Vulnerabilities)는 미국 CISA가 "실제로 악용되고 있다"고 확인한 취약점 목록입니다. 취약점 공지는 매일 나오니, KEV에 오른 것부터 먼저 고치면 순서가 정해집니다.

롯데카드 사고의 서버는 WebLogic 미들웨어였습니다. 앱 의존성으로 만든 SBOM만으로는 이런 서버를 잡지 못합니다. 쓰지 않는 서버까지 자산 목록에 올리는 일이 함께 필요합니다.

막는 코드

특정 회사 코드가 아니라 개념을 보여 주는 예시입니다. 값은 가짜입니다.

<plugin>
  <groupId>org.cyclonedx</groupId>
  <artifactId>cyclonedx-maven-plugin</artifactId>
  <executions>
    <execution><phase>package</phase><goals><goal>makeAggregateBom</goal></goals></execution>
  </executions>
</plugin>
// CI 단계: SBOM에서 찾은 CVE 가운데 KEV에 오른 것이 있으면 빌드 실패
Set<String> kev = KevCatalog.load(Path.of("kev.json")).cveIds();
List<Finding> hits = findings.stream()                // 취약점 스캐너가 SBOM에서 찾은 결과
        .filter(f -> kev.contains(f.cveId()))
        .toList();
if (!hits.isEmpty()) {
    hits.forEach(f -> log.error("KEV {} in {}", f.cveId(), f.component()));
    System.exit(1);
}

KevCatalog와 Finding은 설명용 이름입니다.

확인하는 법

알려진 취약 버전을 일부러 넣은 브랜치에서 빌드가 실패하는지 확인합니다. KEV 목록이 갱신된 날부터 반영까지 걸린 시간을 기록해 둡니다.

2. CI 도구 고정과 단기 자격증명

무엇이 문제인가

CI 설정에서 외부 도구를 @v4 같은 버전 태그로 받으면, 태그가 가리키는 코드는 바뀔 수 있습니다. 설정 파일은 그대로인데 다음 실행부터 다른 코드가 돕니다. EU 집행위원회 사고 때는 trivy-action의 태그 76개 중 75개가 악성 코드를 가리키도록 바뀌었습니다(위즈 분석). 그 CI 환경에는 오래 쓰는 AWS 액세스 키가 있었고, 그 키에는 다른 집행위 AWS 계정을 관리할 권한까지 있었습니다(CERT-EU). CERT-EU 권고의 첫 번째 항목은 "전체 커밋 해시로 고정"입니다.

막는 코드

# CI 워크플로(GitHub Actions 문법). 해시 값은 가짜
permissions:
  contents: read
  id-token: write                # 실행할 때만 받는 단기 자격증명(OIDC)
steps:
  - uses: actions/checkout@0000000000000000000000000000000000000001   # 전체 커밋 해시
  - uses: actions/setup-java@0000000000000000000000000000000000000002 # 태그 대신 해시
  - run: ./mvnw -B verify

단기 자격증명은 CI가 실행될 때 클라우드에 "이 저장소의 이 워크플로"임을 증명하고 짧은 수명의 키를 받는 방식입니다. CI 설정에 장기 키를 저장하지 않으니 도구가 오염돼도 들고 나갈 키의 수명이 짧습니다. 받는 역할의 권한도 배포에 필요한 범위로 줄입니다.

확인하는 법

uses: 줄 가운데 40자리 해시가 아닌 것이 있으면 CI가 실패하도록 검사 단계를 둡니다. CI 시크릿 목록에 장기 클라우드 키가 남아 있지 않은지 봅니다.

3. 이미지 서명과 배포 전 확인

무엇이 문제인가

배포할 때는 보통 이미지 이름과 태그로 이미지를 받아 옵니다. 태그는 덮어쓸 수 있습니다. 레지스트리 계정 하나가 새면 같은 이름의 다른 이미지가 운영에 올라갑니다. 확인하는 단계가 없으면 배포 스크립트는 바뀐 이미지도 그대로 실행합니다.

막는 코드

# deploy.sh: 서명이 맞아야 배포한다 (레지스트리 주소는 가짜)
IMAGE=registry.example.com/daon:1.4.2
cosign verify --key cosign.pub "$IMAGE" || exit 1
docker compose up -d app

빌드 직후 CI에서 이미지에 서명하고, 배포 스크립트는 공개키로 그 서명을 확인합니다. 서명용 개인키는 시크릿 저장소에 두거나 키 없는 서명(OIDC 기반)을 씁니다. 태그 대신 이미지 다이제스트(@sha256:…)로 배포하면 확인한 이미지와 실행하는 이미지가 같다는 점도 보장됩니다.

확인하는 법

서명하지 않은 테스트 이미지를 같은 태그로 올린 뒤 배포가 멈추는지 확인합니다.

4. 신뢰할 수 없는 데이터 역직렬화

무엇이 문제인가

역직렬화(deserialization)는 바이트나 문자열을 프로그램 객체로 되돌리는 일입니다. Java의 ObjectInputStream은 데이터 안에 적힌 클래스 이름대로 객체를 만듭니다. 받는 쪽 코드가 Cart로 형변환하기 전에, 클래스패스에 있는 다른 클래스의 객체가 이미 만들어지고 그 클래스의 코드가 실행될 수 있습니다. 요청 본문, 쿠키, 메시지 큐처럼 밖에서 오는 데이터를 이렇게 복원하면 서버에서 의도하지 않은 코드가 돌 수 있습니다. JSON 라이브러리에서 "데이터가 클래스 이름을 정하는" 기능(Jackson의 기본 타이핑 등)을 켜도 같은 위험이 생깁니다.

막는 코드

// 위험한 모양: 요청 본문을 Java 객체로 그대로 복원
@PostMapping("/cart/import")
public void importCart(HttpServletRequest req) throws Exception {
    try (ObjectInputStream in = new ObjectInputStream(req.getInputStream())) {
        Cart cart = (Cart) in.readObject();   // 어떤 클래스를 만들지 데이터가 정한다
    }
}
// 고친 모양: 형식을 고정한 DTO로만 받는다(JSON, 기본 타이핑 끔)
public record CartImport(@Size(max = 100) List<Item> items) {
    public record Item(long productId, @Min(1) int qty) {}
}

@PostMapping("/cart/import")
public void importCart(@Valid @RequestBody CartImport body) { cartService.importItems(body); }

// Java 직렬화를 피할 수 없다면 허용 타입만 받는다
in.setObjectInputFilter(ObjectInputFilter.Config.createFilter(
        "maxdepth=5;com.example.cart.*;java.util.ArrayList;!*"));

업데이트 파일이나 원격 설정을 받는 경우에는 서명을 확인한 뒤에만 읽습니다.

확인하는 법

코드에서 ObjectInputStream, readObject, activateDefaultTyping, @JsonTypeInfo(use = Id.CLASS)를 검색해 밖에서 오는 데이터에 쓰인 곳이 없는지 봅니다. 허용 목록에 없는 타입이 들어오면 InvalidClassException이 나는지 테스트로 확인합니다.

점검 체크리스트

  • □ 빌드마다 SBOM이 만들어지고, KEV에 오른 취약점이 있으면 빌드가 실패한다
  • □ 쓰지 않는 서버와 미들웨어도 자산 목록에 있고 패치 날짜가 기록된다
  • □ CI의 외부 도구는 전체 커밋 해시로 고정돼 있고, 장기 클라우드 키 대신 단기 자격증명을 쓴다
  • □ 이미지는 빌드 직후 서명되고, 배포 스크립트는 서명을 확인하지 못하면 멈춘다
  • □ 밖에서 오는 데이터는 형식을 고정한 DTO로만 받고, Java 직렬화에는 허용 타입 필터가 있다

정리

목록에 없는 서버와 부품은 아무도 패치하지 않습니다. SBOM과 KEV 대조로 무엇이 들어 있고 무엇을 먼저 고칠지 정합니다. CI 도구는 확인한 그 버전만 실행하도록 커밋 해시로 고정하고, CI에 오래 사는 키를 두지 않습니다. 운영에는 서명을 확인한 이미지만 올리고, 실행 중에는 밖에서 온 데이터가 만들 객체의 종류를 서버가 정합니다.

함께 보면 좋은 글

보안을 더 알고 싶다면

오늘 본 사고의 원인은 대부분 서버와 코드의 기본기에서 막을 수 있는 것들이었습니다. 직접 손으로 막아 보고 싶다면 이런 강의가 있습니다.

실제 해킹 사고로 배우는 Spring Boot·Express 시큐어 코딩

  • AI로 자동화된 해킹 시대, 오픈소스 쇼핑몰 코드에서 실제 사고 22건을 재연하고 직접 취약점을 찾아 한 건씩 막습니다.
  • OWASP Top 10:2025 전체와 목록 밖 위험 20가지를 102개 수업으로 다루고, 취약점 37개를 모두 막은 최종 소스를 가져갑니다. Spring Boot와 Express 모두 다룹니다.
  • 이 글과 이어지는 수업: 7-1 ~ 7-7

서버 보안 기초부터 차근차근 시작하고 싶다면 이 강의도 있습니다.

백엔드 개발자를 위한 서버 보안 실무: 해킹 방어부터 정보유출 방지까지

  • 서버를 공격하는 흐름을 로그로 보고, 내 서버와 Spring·Express 코드에서 직접 막아 봅니다.
  • SSH 하드닝, 인젝션, 인증·인가, 시크릿과 개인정보 보호, 침해사고 첫 한 시간까지 8개 섹션 54개 수업입니다.
  • 이 글과 이어지는 수업: 2-7, 6-3, 6-4, 7-3

출처와 확인 날짜

수치와 사실은 2026년 11월 2일 기준 공개 자료입니다. 이 글은 법률 자문이 아닙니다. EU 집행위원회 사고의 경로는 CERT-EU가 "높은 확신"으로 평가한 것이고, 태그 변경 수는 위즈의 분석입니다.


이전 글 코드 밖에 둘 것: 시크릿 저장소, 열린 저장소, 오류 응답과 보안 헤더
다음 글 7개월을 줄이는 기록: 감사 로그, 대량 조회 탐지, 마스킹, 지울 수 없는 로그
시리즈 전체 목록 해킹 방식 VS 방어 방법 전체 목록

profile
고인물 개발자(OG)

0개의 댓글