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


직렬화는 위험하다.

자바의 역직렬화는 명백하고 현존하는 위험이다. 이 기술은 지금도 애플리케이션에서 직접 혹은, 자바 하부 시스템(RMI(Remote Method Invocation), JMX(Java Management Extension), JMS(Java Messaging System) 같은) 을 통해 간접적으로 쓰이고 있기 때문이다. 신뢰할 수 없는 스트림을 역직렬화하면 원격 코드 실행(remote code execution, RCE), 서비스 거부(denial-of-service, DoS)등의 공격으로 이어질 수 있다. 잘못한 게 없는 애플리케이션이라도 이런 공격에 취약해질 수 있다. - CERT 조정 센터의 기술관리자 로버트 시커드(Robert Seacord)

자바에선 ObjectInputStream의 readObject 메서드를 통해 객체 그래프가 역직렬화 되는데, 문제는 바이트 스트림을 역직렬화 하는 과정에서 그 타입들 안의 모든 코드를 수행할 수 있다는 것인데, 이는 코드 전체가 공격범위에 들어간다는 것이다.

모든 직렬화 가능 클래스들을 공격에 대비하도록 작성해도 애플리케이션을 취약해질 수 있다.

다음은 바우터르 쿠카르츠(Wouter Coekaerts)라는 기술자가 만든 역직렬화 폭탄(deserialization bomb) 코드이다.

static byte[] bomb() {
    Set<Object> root = new HashSet<>();
    Set<Object> s1 = root;
    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;
    }
    
    return serialize(root); //직렬화 메서드 코드(serialize)는 생략 
}

이 bomb() 메서드의 root 객체 그래프는 201개의 HashSet 인스턴스로 구성되어 각각 3개 이하의 객체 참조를 가진다. 전체 크기는 크지 않지만, 역직렬화는 영원히 끝나지 않을 것이다. 그 이유는 각각의 원소들의 해시코드를 계산해야 하는 데 걸리는 시간 때문인데, 깊이가 100단계이고 각각의 원소가 HashSet인 컬렉션이 되는데 이 HashSet을 역직렬화 하기 위해서는 hashCode 메서드를 $2^{100}$번 넘게 호출해야 한다. 그럼 이러한 문제들은 어떻게 해야 할까?

직렬화를 사용하지 말자.

신뢰불가한 바이트 스트림을 역직렬화 하는 작업은 스스로를 공격에 노출하는 행위로 애초에 역직렬화나 직렬화를 사용하지 않는게 가장 안전할 수 밖에 없다.

요즘 객체와 바이트 시퀀스를 변환해주는 다른 기술들이 많은데, 직렬화 시스템 혹은 크로스-플랫폼 구조화된 데이터 표현(cross-platform structured-data representation)이라 한다.

이러한 방식은 임의 객체 그래프를 직렬화/역직렬화 하는 대신 기본 타입 몇 개와 배열 타입만 지원한다. 하지만, 이정도의 추상화로도 충분히 사용할 수 있고 직렬화의 문제도 회피할 수 있다. 이런 데이터 표현의 선두주자로는 JSON과 프로토콜 버퍼(Protocol Buffers, protobuf)가 있다.

JSON