도당탕탕

Item85 : 자바 직렬화의 대안을 찾으라 본문

JAVA

Item85 : 자바 직렬화의 대안을 찾으라

backlo 2023. 2. 8. 15:35

직렬화는 프로그래머가 어렵지 않게 분산 객체를 만들 수 있다고 해도, API와 구현 사이의 모호해진 경계, 잠재적인 정확성 문제, 성능, 보안, 유지보수 등 직렬화는 다양한 위험성을 내포하고 있다.

대표적인 사례를 보자면 샌프란시스코 시영 교통국이 랜섬웨어 공격을 받아 요금 징수 시스템이 이틀간 마비되는 사태를 겪었다고 한다. 그렇기 때문에 직렬화는 되도록 피하는 것이 좋다.

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

예를 들면 ObjectInputStreamreadObject 메서드를 호출하면서 객체 그래프가 역직렬화를 한다. readObject 메소드는메서드는 클래스패스 안의 거의 모든 타입의 객체를 만들어 낼 수 있기 때문에, 바이트 스트림을 역직렬화하는 과정에서 이 메서드는 그 타입들 안의 모든 코드를 수행할 수 있다. 즉, 그 타입들의 코드 전체가 공격 범위에 들어가 범위가 넓어져 결국 방어하기 힘들어진다는 점이다. 이는 자바의 표준 라이브러리뿐만 아니라 애플리케이션에 치명적으로 침투할 수 있다.

한편 역직렬화 과정에서 호출되어 잠재적인 위험한 동작을 수행하는 메서드를 가젯(gadget) 이라고 한다. 하나의 가젯이 또는 여러 개의 가젯이 마음대로 코드를 수행하게 할 수 있다. 따라서 아주 신중하게 제작된 바이트 스트림만 역직렬화해야 한다.

역직렬화 폭탄

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

  • 역직렬화 폭탄
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);
}

serialize 메소드가 실행되기 전의 인스턴스 참조 형태를 보면 root의 0번째와 1번째의 HashSet의 가지고 있고 또 그 HashSet에는 0번째와 1번째의 HashSet을 가지고 있는 이러한 반복된 구조로 이루어지게 된다.

  • 위 예제의 결과
// root
//    0 = HashSet
//        0 = HashSet
//            0 = HashSet
//                 ....
//            1 = HashSet
//        1 = HashSet
//    1 = HashSet

위 결과는 100단계까지 만들어지며, 이를 역직렬화하면 2^100번 넘게 호출해야 한다. 이는 역직렬화가 영원히 계속된다는 문제점도 있지만, 무언가 잘못되었다는 신호조차 주지 않는다는 것도 큰 문제다.

역직렬화 대처

그렇다면 이 문제들을 어떻게 해결하면 좋을까? 가장 좋은 방법은 아무것도 자바 역직렬화하지 않는 것이다. 그리고 크로스-플랫폼 구조화된 데이터 표현 방법을 사용하는 것이다. 이 방법들은 공통점으로 자바 직렬화보다 훨씬 간단하다는 것이다. 임의 객체 그래프를 자동으로 직렬화/역직렬화를 하지 않는다. 대신 속성-값 쌍의 집합으로 구성된 간단하고 구조화된 데이터 객체를 사용한다.

예를 들어 JSON, protocol buffer 등이 있다. 프로토콜 버퍼는 이진 표현이라 효율이 훨씬 높으며 JSON은 텍스트 기반이라 사람이 읽을 수 있는 장점이 있다. 또한 JSON은 오직 데이터를 표현하는 데만 쓰이지만, 프로토콜 버퍼는 문서를 위한 스키마를 제공하고 올바르게 쓰도록 강조한다.

이렇게 다양한 방법으로 자바 직렬화가 가져온 심각한 문제들을 회피할 수 있고 강력한 분산 시스템을 구축할 수 있게 해 준다.

레거시 시스템 때문에 자바 직렬화를 완전히 배제할 수 없을 때의 차선책은 신뢰할 수 없는 데이터는 절대 역직렬화하지 않는 것이다. 특히, 신뢰할 수 없는 발신원으로부터의 RMI는 절대 수용해서는 안된다.

직렬화를 피할 수 없고 역직렬화한 데이터가 안전하지 완전히 확신할 수 없다면 객체 역직렬화 필터링을 사용하자. ( java 9에서 추가되었고 이전 버전에서 사용 가능 ). 객체 역직렬화 필터링은 데이터 스트림이 역직렬화되기 전에 필터를 설치하는 기능이다. 즉 특정 클래스만 수용하거나 제외할 수 있도록 하는 기능이다.

이처럼 자바 직렬화는 위험요소가 많다. 그러니 만약 직렬화를 사용하는 시스템을 관리해야 한다면 시간과 노력을 들여 크로스 플랫폼으로 마이그레이션 하는 것을 추천한다.

Comments