직렬화의 근본적인 문제는 공격 범위가 너무 넓고 지속적으로 더 넓어져 방어하기 어렵다는 점이다. ObjectInputStream 의 readObject 메서드를 호출하서 객체 그래프가 역직렬화되기 때문이다. readObject 메서드는 (Serializable 인터페이스를 구현했을 시) 클래스패스 안의 거의 모든 타입의 객체를 만들어 낼 수 있다.
바이트 스트림을 역직렬화하는 과정에서 이 메서드는 그 타입들 안의 모든 코드를 수행할 수 있다.
→ 그 타입들의 코드 전체가 공격 범위에 들어간다는 뜻이다.
자바의 표준 라이브러리나 아파치 커먼즈 컬렉션 같은 서드파티 라이브러리는 물론 애플리케이션 자신의 클래스들도 공격 범위에 포함된다.
관련한 모든 모범 사례를 따르고 모든 직렬화 가능 클래스들을 공격에 대비하도록 작성한다 해도, 애플리케이션은 여전히 취약할 수 있다.
신뢰할 수 없는 스트림을 역직렬화하면 원격 코드 실행(remote code execution, RCE), 서비스 거부(Dos)등의 공격으로 이어질 수 있다.
직렬화 가능 타입들을 연구하여 역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 메서드들을 찾을 수 있다. 이런 메서드를 가젯(gadget)이라 부른다.
여러 가젯을 함께 사용하여 가젯 체인을 구성할 수도 있다. 가끔씩 공격자가 기반 하드웨어의 네이티브 코드를 마음대로 실행할 수 있는 아주 강력한 가젯 체인도 발견되곤 한다.
그래서 아주 신중하게 제작한 바이트 스트림만 역직렬화 해야 한다.
역직렬화에 시간이 오래 걸리는 짧은 스트림을 역직렬화하는 것만으로도 서비스 거부 공격에 쉽게 노출될 수 있다. 이런 스트림을 역직렬화 폭탄(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의 100거듭제곱으로 넘게 호출해야 한다.
역직렬화가 영원히 계속된다는 것도 문제지만, 신호조차 주지 않는다는 것도 큰 문제다.
→ 이 코드는 단 몇 개의 객체만 생성해도 스택 깊이 제한에 걸려버린다.
새로운 시스템에서 자바 직렬화를 써야 할 이유는 전혀 없다. 객체와 바이트 시퀀스를 변환해주는 다른 메커니즘이 많이 있다. 직렬화 시스템이라 불리기도 하지만, 크로스-플랫폼 구조화된 데이터 표현이라 한다.
이 표현들의 공통점은 자바 직렬화보다 훨씬 간단하다는 것이다. 임의 객체 그래프를 자동으로 직렬화/역직렬화하지 않는다. 대신 속성-값 쌍의 집합으로 구성된 간단하고 구조화된 데이터 객체를 사용한다.
그리고 기본 타입 몇 개와 배열 타입만 지원할 뿐이다. 이런 간단한 추상화만으로도 아주 강력한 분산 시스템을 구축하기에 충분하고, 자바 직렬화가 가져온 심각한 문제들을 회피할 수 있다.
JSON은 브라우저와 서버의 통신용으로 설계되었다.
프로토콜 버퍼는 구글이 서버 사이에 데이터를 교환하고 저장하기 위해 설계했다.
보통은 이들을 언어 중립적이라고 하지만, 사실 JSON은 자바스크립트용으로, 프로토콜 버퍼는 C++용으로 만들어졌다.
둘의 가장 큰 차이는 JSON은 텍스트 기반으로 사람이 읽을 수 있고, 프로토콜 버퍼는 이진 표현이라 효율이 훨씬 높다는 점이다. 또한 JSON은 오직 데이터를 표현하는 데만 쓰이지만, 프로토콜 버퍼는 문서를 위한 스키마(타입)를 제공하고 올바로 쓰도록 강요한다.
효율은 프로토콜 버퍼가 훨씬 좋지만 텍스트 기반 표현에는 JSON이 아주 효과적이다. 또한, 프로토콜 버퍼는 이진 표현뿐 아니라 사람이 읽을 수 있는 텍스트 표현(pbtxt)도 지원한다.
레거시 시스템 때문에 자바 직렬화를 완전히 배제할 수 없을 때의 차선책은
신뢰할 수 없는 데이터는 절대 역직렬화하지 않는 것이다.
특히, 신뢰할 수 없는 발신원으로부터의 RMI는 절대 수용해서는 안된다. 자바의 공식 보안 코딩 지침에서는 "신뢰할 수 없는 데이터의 역직렬화는 본질적으로 위험하므로 절대로 피해야 한다" 라고 조언한다.
객체 역직렬화 필터링은 데이터 스트림이 역직렬화되기 전에 필터를 설치하는 기능이다.
클래스 단위로, 특정 클래스를 받아들이거나 거부할 수 있다. '기본 수용' 모드에서는 블랙리스트에 기록된 잠재적으로 위험한 클래스들을 거부한다. 반대로 '기본 거부' 모드에서는 화이트리스트에 기록된 안전하다고 알려진 클래스들만 수용한다.
→ 블랙리스트 방식보다는 화이트리스트 방식을 추천한다.
화이트리스트를 자동으로 생성해주는 스왓(SWAT, Serial Whitelist Application Trainer)라는 도구가 있으니 참고할 것.
필터링 기능은 메모리를 과하게 사용하거나 객체 그래프가 너무 깊어지는 사태로부터도 보호해준다.
→ 직렬화 폭탄은 걸러내지 못한다.