신뢰할 수 없는 바이트를 객체로 되살리는 것 자체가 코드 실행 통로가 된다.
왜 위험한가
자바 역직렬화는 단순히 필드를 채우는 것이 아니다
- readObject() · readResolve() 같은 훅이 호출된다
- 그 안에서 다른 객체의 메서드가 연쇄로 불린다
공격자는 클래스패스에 이미 있는 클래스들을 조합해 원하는 동작에 도달하는 경로(가젯 체인) 를 만든다
- 새 코드를 주입할 필요가 없다. '있는 것' 만으로 충분하다
- commons-collections 의 InvokerTransformer 가 대표적 가젯이었다
검증으로는 못 막는 이유
"역직렬화한 뒤에 검사하겠다" → 이미 늦었다 객체를 만드는 과정 자체에서 코드가 돈다
"타입을 확인하겠다" → 스트림을 읽어야 타입을 알 수 있다
- 읽는 동안 이미 실행된다
방어
- ① 신뢰 못 할 입력을 자바 직렬화로 받지 않는다 (근본 대응)
- JSON · Protobuf 처럼 '데이터만' 표현하는 포맷으로 바꾼다
- ② 불가피하면 허용 목록 기반 필터
- ObjectInputFilter (Java 9+) / -Djdk.serialFilter
- 차단 목록은 새 가젯이 나오면 뚫린다 — 반드시 허용 목록으로
- ③ 필드에 transient 를 다는 것은 방어가 아니다 (정보 노출만 줄인다)
언어 차원의 결론
자바 직렬화는 설계상 결함으로 평가되어 '제거 대상' 으로 분류됐다
- JEP 154 로 일부 기능이 이미 폐기 경로에 올랐다
- record 는 직렬화 시 생성자를 거치므로 상대적으로 안전하다
Effective Java 3판의 결론도 같다 — "자바 직렬화를 새로 쓰지 마라"
실무에서 남아 있는 곳
세션 저장소, 분산 캐시, RMI, JMX, 일부 메시지 브로커 페이로드. 외부에 노출된 포트에서 자바 직렬화를 받고 있지 않은지 확인하는 것이 점검의 출발점이다.
JSON 도 안전한 것은 아니다
"자바 직렬화 대신 JSON 을 쓰면 된다" 는 절반만 맞다
Jackson 의 다형 역직렬화(@JsonTypeInfo · enableDefaultTyping) 를 켜면 JSON 안의 타입 정보를 보고 임의 클래스를 인스턴스화한다
- 같은 가젯 문제가 그대로 재현된다 (CVE 가 여러 건 나왔다)
안전한 사용
- 기본 타이핑을 켜지 않는다
- 켜야 하면 PolymorphicTypeValidator 로 허용 타입을 못 박는다
- 역직렬화 대상은 DTO 로 한정하고 엔티티를 직접 받지 않는다
점검 체크리스트
□ ObjectInputStream 을 외부 입력에 쓰는 곳이 있는가 □ 세션을 직렬화해 외부 저장소에 넣는가 (저장소가 뚫리면 RCE 로 이어진다) □ RMI · JMX 포트가 외부에 열려 있는가 □ 캐시(예: 분산 캐시) 직렬화 방식이 자바 직렬화인가 □ jdk.serialFilter 가 설정돼 있는가
가장 빠른 점검은 "역직렬화 지점이 어디인가" 를 먼저 목록화하는 것이다
면접 함정
- ❌ "Serializable만 안 붙이면 안전" → 클래스패스의 다른 클래스가 가젯이 된다.
- ❌ "차단 목록으로 충분" → 새 가젯이 계속 발견된다.