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(객체 역직렬화 필터링)를 사용하되, 이마저도 모든 공격에서 안전하지 않음을 기억하자.
- 클래스가 직렬화 지원 불가하게 만들고, 이게 불가능하다면 정말 신경써서 작성해야 한다.
Comments powered by Disqus.