아이템 85. 자바 직렬화의 대안을 찾으라

2025. 3. 12. 19:31

직렬화: 객체 -> 바이트 스트림

역직렬화: 바이트 스트림 -> 객체

직렬화된 객체는 다른 VM에 전송하거나 디스크에 저장한 후 나중에 역직렬화할 수 있다.

 

직렬화를 통해 프로그래머가 어렵지 않게 분산 객체를 만들 수 있다는 취지였지만, 실상은 보이지 않는 생성자, API와 구현 사이의 모호해진 경계, 성능, 보안 등 그 대가가 컸다. 지금까지 경험한 바로는 단점이 더 컸다.

 

직렬화의 근본적인 문제는 공격 범위가 너무 넓고 지속적으로 넓어져 방어하기 어렵다는 점이다. 이는 역직렬화될 때, ObjectInputStream의 readObject 메서드가 호출되기 때문인데, 이 메서드는 클래스패스 안의 거의 모든 타입의 객체를 만들어 낼 수 있는, 마법 같은 생성자이기 때문이다.

 

가젯(gadget): 역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 메서드.

가끔 공격자가 기반 하드웨어의 네이티브 코드를 마음대로 실행할 수 있는 아주 강력한 가젯 체인도 발견된다. 그래서 아주 신중하게 제작한 바이트 스트림만 역직렬화 해야 한다.

 

역직렬화에 시간이 오래 걸리는 짧은 스트림을 역직렬화하는 것만으로도 서비스 거부 공격에 쉽게 노출될 수 있다. 이런 스트림을 역직렬화 폭탄이라고 한다.

public static void main(String[] args) {
    Set<Object> root = new HashSet<>();
    Set<Object> s1 = new HashSet<>();
    Set<Object> s2 = new HashSet<>();
    for (int i = 0; i < 100; i++) {
        Set<Object> t1 = new HashSet<>();
        Set<Object> t2 = new HashSet<>();
        t1.add("foo");
        s1.add(t1);
        s1.add(t2);
        s2.add(t1);
        s2.add(t2);
        s1 = t1;
        s2 = t2;
    }
}
이 코드는 한 Set당 3개 이하의 객체 참조를 가진다. 이 코드를 역직렬화하려면 원소들의 해시 코드를 계산해야 하는데, 
그러기 위해선 hashCode 메서드를 2의 100승 번 계산해야 한다.

이 문제를 피하는 가장 좋은 방법은 아무것도 역직렬화하지 않는 것이다. = 승리하는 유일한 길은 전쟁하지 않는 것이다.

우리가 작성하는 새로운 시스템에서 자바 직렬화를 써야 할 이유는 전혀 없다. 객체와 바이트 시퀀스를 변환해주는 다른 메커니즘이 많이 있다.(JSON, protobuf 등) 
이 표현들은 임의 객체 그래프를 자동으로 직렬화/역직렬화하지 않고 대신 속성-값 쌍의 집합으로 구성된 간단하고 구조화된 데이터 객체를 사용한다.

 

JSON은 텍스트 기반이라 사람이 읽을 수 있고, 프로토콜버퍼는 이진 표현이라 효율이 훨씬 높다.

 

만약 레거시 시스템으로 인해 자바 직렬화를 완전히 배제할 수 없다면, 차선책은 신뢰할 수 없는 데이터는 절대 역직렬화하지 않는 것이다. 역직렬화한 데이터가 안전한지 완전히 확신할 수 없다면 객체 역직렬화 필터링을 사용하자. <- 데이터 스트림이 역직렬화되기 전에 필터를 설치하는 기능이다. 블랙리스트 방식(기록된 클래스만 거부)보다 화이트리스트 방식(기록된 클래스만 허용)이 더 낫다. 필터링 기능은 메모리 과하게 사용하거나 객체 그래프가 너무 깊어지는 사태를 보호해준다. 하지만 앞서의 직렬화 폭탄은 걸러내지 못한다.

BELATED ARTICLES

more