Home [Effective Java] Item85. 자바 직렬화의 대안을 찾으라!
Post
Cancel

[Effective Java] Item85. 자바 직렬화의 대안을 찾으라!

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

객체 직렬화

  • 자바가 객체를 바이트 스트림으로 인코딩하고(직렬화) 그 바이트 스트림으로부터 다시 객체를 재구성하는(역직렬화) 매커니즘이다.
  • 직렬화된 객체는 다른 VM에 전송하거나 디스크에 저장한 후 나중에 역직렬화가 가능하다.
  • 직렬화는 여러 위험을 품고 있고, 이러한 위험을 알아두고, 그 위험을 최소화하면 좋을 것이다.

역직렬화의 위험

  • 직렬화의 근본적인 문제는 공격 범위가 너무 넓고, 지속적으로 더 넓어진다는 것. 즉 방어가 어렵다는 점이다.
  • ObjectInputStream의 readObject 메서드를 호출하면서 객체 그래프가 역직렬화되기 때문이다.
    • readObject 메서드는 (Serializable 인터페이스를 구현했다면) 클래스패스 안의 거의 모든 타입의 객체를 만들어낼 수 있는, 사실상 마법 같은 생성자다.
    • 바이트 스트림 역직렬화 과정에서 이 메서드는 그 타입들 안의 모든 코드 수행이 가능하다. 즉, 그 타입들의 코드 전체가 공격 대상이 될 수 있다.
  • 자바 표준 라이브러리, 아파치 커먼즈 컬렉션 등 서드파티는 물론 애플리케이션 자신의 클래스들도 공격 범위에 포함된다.
    • 관련 모범 사례를 따르고, 모든 직렬화 가능 클래스가 공격 대비를 마쳐도, 여전히 취약할 수 있다.

가젯(gadget)

  • 공격자와 보안 전문가들은 자바 라이브러리와 널리 쓰이는 서드파티에서 직렬화 가능 타입을 연구했다.
  • 역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 메서드를 찾아보았고, 이러한 메서드를 가젯이라 부른다.
  • 가젯 여러 개를 함께 사용해 가젯 체인을 구성할 수 있고, 이를 통해 공격자는 기반 하드웨어의 네이티브 코드를 마음대로 실행하는 공격도 할 수 있다.
    • 실제로 샌프란시스코 교통국을 마비시킨 공격이 이러한 사례다.

역직렬화 폭탄(deserialization bomb)

  • 가젯까지 갈 필요도 없이, 역직렬화에 시간이 오래 걸리는 짧은 스트림을 역직렬화하는 것만으로도 서비스 거부 공격에 쉽게 노출될 수 있다.

    • 이러한 스트림을 역직렬화 폭탄이라 한다.
  • 예시

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    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); // 간결하게 하기 위해 이 메서드의 코드는 생략함
    }
    
    • 바우터르쿠카르츠가 HashSet과 문자열만 사용해 만든 예제다.
    • 이 스트림의 역직렬화는 영원히 계속된다.
    • 이 객체 그래프는 201개의 HashSet 인스턴스로 구성되며, 그 각각은 3개 이하의 객체 참조를 갖는다. 스트림의 전체 크기는 5,744바이트지만, 역직렬화는 끝나지 않을 것이다.
      • HashSet 인스턴스를 역직렬화하려면 그 원소들의 해시코드를 계산해야 한다. 이것이 문제다. root에 담긴 두 원소는 각각 (루트와 마찬가지로) 다른 HashSet 2개씩을 원소로 갖는 HashSet이다. 그리고 반복문에 의해 이 구조가 깊이 100단계까지 만들어진다. 이걸 역직렬화하려면 hashCode 메서드를 2^100번 넘게 호출한다. 역직렬화가 영원히 계속된다(그런데, 여기서 무언가 잘못되었다는 신호조차 주지 않는다). 이 코드는 단 몇 개의 객체만 생성해도 스택 깊이 제한에 걸려버린다.

문제 해결법

  • 직렬화 위험을 회피하는 가장 좋은 방법은 마무것도 역직렬화하지 않는 것이다.
    • 새롭게 작성하는 시스템에서 자바 직렬화를 써야 할 이유는 전혀 없다.
    • 필요하다면 객체와 바이트 시퀀스를 변환해주는 다른 매커니즘이 많으니 이를 사용하자.
  • 직렬화를 피할 수 없고, 역직렬화한 데이터가 안전한지 완전히 확신할 수 없는 경우, 객체 역직렬화 필터링(java.io.ObjectInputFilter)을 사용하자.
    • 자바 9에서 추가되었고, 이전 버전에서도 쓸 수 있도록 이식되었다.
    • 이를 통해 특정 클래스를 받아들이거나 거부할 수 있다.
      • 기본적으로는 수용하고, 블랙리스트만 거부하는 방법과, 기본적으로는 거부하고, 안전하다고 알려진 화이트리스트만 허용하는 방법이 있다.
      • 블랙리스트 방식보다는 화이트리스트 방식을 사용하자.

핵심 정리

  • 직렬화는 위험하니 피하자.
  • 시스템을 밑바닥부터 설계한다면 JSON이나 프로토콜 버퍼같은 대안을 사용하자.
  • 신뢰할 수 없는 데이터는 역직렬화하지 말자.
  • 꼭 역직렬화해야 한다면 ObjectInputFileter(객체 역직렬화 필터링)를 사용하되, 이마저도 모든 공격에서 안전하지 않음을 기억하자.
  • 클래스가 직렬화 지원 불가하게 만들고, 이게 불가능하다면 정말 신경써서 작성해야 한다.
This post is licensed under younghwani by the author.

[Effective Java] Item84. 프로그램의 동작을 스레드 스케줄러에 기대지 말라!

[Env] 사용중인 포트 찾아서 죽이기

Comments powered by Disqus.