DEV ℧ Developer Diary

[EffectiveJava] item85 - 자바 직렬화의 대안을 찾으라

직렬화의 도입

1997년, 자바에 처음으로 직렬화가 도입되었다. 처음엔 다소 위험하다는 이야기가 나왔지만, 프로그래머가 어렵지 않게 분산 객체를 만들 수 있다는 구호는 매력적이었고, 지지자들은 장점이 위험성을 압도한다고 생각했다.

직렬화의 위험성

보안 문제는 실제로도 우려한 만큼 심각한 것으로 밝혀졌다. 2000년대 초반에 논의된 취약점들이 그 후로 십 년 이상 심각하게 악용 되었다.

직렬화의 근본적 문제

직렬화의 근본적인 문제는 공격 범위가 너무 넓고 지속적으로 더 넓어져 방해하기 어렵다는 점이다.

ObjectInputStreamreadObject 메서드를 호출하면서 객체 그래프가 역직렬화되기 때문이다. readObject 메서드는 (Serializable 인터페이스를 구현했다면) 클래스패스안의 거의 모든 타입의 객체를 만들어 낼 수 있는, 사실상 마법 같은 생성자다.

바이트 스트림을 역직렬화하는 과정에서 이 메서드는 그 타입들 안의 모든 코드를 수행할 수 있다. 이말은 타입들의 모든 코드 전체가 공격 범위에 들어간다는 뜻이다.

역직렬화 폭탄(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") /** t1을 t2와 다르게 만든다. */
        s1.add(t1); s1.add(t2);
        s2.add(t1); s2.add(t2);
        s1 = t1;
        s2 = t2;
    }
    return serialize(root); /** 간결하게 하기 위해 이 메서드의 코드는 생략함 */
}

이 객체 그래프는 201개의 HashSet 인스턴스로 구성되며, 그 각각은 3개 이하의 객체 참조를 갖는다.
스트림의 전체 크기는 5,744바이트지만, 역직렬화는 태양이 불타 식을 때까지도 끝나지 않을 것이다.

역직렬화 폭탄의 원인

문제는 HashSet 인스턴스를 역직렬화 하려면 그 원소들의 해시코드를 계산해야 한다는 데 있다.

루트 HashSet에 담긴 두 원소는 각각 다른 HashSet 2개씩을 원소로 갖는 HashSet이다. 그리고 반복문에 의해 이 구조가 깊이 100단계까지 만들어진다.
따라서 이 HashSet을 역직렬화 하려면 hashCode메서드를 2¹⁰⁰번 넘게 호출해야한다.

직렬화의 위험 대처

직렬화 위험을 회피하는 가장 좋은 방법은 아무것도 역직렬화하지 않는 것이다. 신뢰할 수 없는 바이트 스트림을 역직렬화하는 일 자체가 스스로를 공격에 노출하는 행위다.

여러분이 작성하는 새로운 시스템에서 자바 직렬화를 써야 할 이유는 전혀 없다. 객체와 바이트 시퀀스를 변환해주는 다른 메커니즘이 많이 있다. 이 방식들은 자바 직렬화의 여러 위험을 회피하면서 성능, 지원도구 등 다양한 이점을 제공한다.

크로스-플랫폼 구조 데이터

크로스-플랫폼 구조화된 데이터 표현의 선두주자는 JSON과 프로토콜 버퍼(Protocol Buffers 혹은 짧게 protobuf)다.

JSON : 더글라스 크록퍼드가 브라우저와 서버의 통신용으로 설계했다. 프로토콜 버퍼 : 구글이 서버 사이에 데이터를 교환하고 저장하기 위해 설계했다.

이들은 언어 중립적이라고 하지만, JSON은 자바스크립트용, 프로토콜 버퍼는 C++ 용으로 만들어 졌고, 흔적이 남아있다.

크로스-플랫폼 구조의 이점

JSON

  • 텍스트 기반이라 사람이 읽을 수 있다.
  • 오직 데이터를 표현하는 데만 쓰인다.
  • 텍스트 기반 표현에는 아주 효과적이다.

프로토콜 버퍼

  • 이진 표현이라 효율이 훨씬 높다.
  • 문서를 위한 스키마(타입)를 제공하고 올바로 쓰도록 강요한다.
  • 이진 표현 뿐 아니라 사람이 읽을 수 있는 텍스트 표현(pbtxt)도 지원한다.

객체 역직렬화 필터링(java.io.ObjectInputFilter)

레거시 시스템으로 인해 직렬화를 배제할 수 없을때는 신뢰할 수 없는 데이터는 절대 역직력화 하지 않는 것이다.

또한 직렬화를 피할 수 없고 역직렬화한 데이터가 안전한지 완전히 확신할 수 없다면 객체 역직렬화 필터링(java.io.ObjectInputFilter)을 사용하자.

객체 역직렬화 필터링은 데이터 스트림을 역직렬화되기 전에 필터를 설치하는 기능이다.

블랙리스트 방식보다는 화이트리스트 방식을 추천한다.

블랙이스트 방식은 이미 알려진 위험으로부터만 보호할 수 있기 때문이다. 반대로 화이트리스트 방식은 안전하다고 알려진 클래스들만 수용한다.