Item85.자바 직렬화의 대안을 찾으라
직렬화(serialization)란 객체를 데이터 스트림으로 만드는 것을 뜻한다.
다시 말해 객체에 저장된 데이터를 스트림에 쓰기(write)위해 연속적인 데이터로 변환하는 것을 말한다.
객체는 참조타입 인스턴스들로 현재 참조되고 있는 메모리 주소를 전달할 수 없기때문에 Byte 형태로 기본형 데이터들로 변환되어 전송된다.
반대로 스트림으로부터 데이터를 읽어서 객체를 만드는 것을 역직렬화(deserialization)라고 한다.
이를 시스템적으로 살펴보면 JVM의 힙(Heap) 혹은 스택(Stack) 메모리에 상주하고 있는 객체 데이터를 직렬화를 통해 바이트 형태로 변환하여 데이터베이스나 파일과 같은 외부 저장소에 저장해두고, 다른 컴퓨터에서 이 파일을 가져와 역직렬화를 통해 자바 객체로 변환해서 JVM 메모리에 적재하는 것.
객체를 저장한다는 것은 새로운 개게는 오직 인스턴스 변수로만 구성되고 객체의 동일성 판정은 두 객체의 인스턴스 변수값이 같은지 판단한다.
클래스 변수나 메서드는 static 영역에 이미 적재되어 있기 때문에 객체를 저장한다는 것은 객체의 모든 인스턴수 변수의 값을 저장한다는 의미이다.
직렬화를 사용하려면 Serializable 인터페이스를 구현해주면 된다.
직렬화 사용처
서블릿 세션(Servlet Session)
세션을 서블릿 메모리 위에서 운용한다면 직렬화가 필요 없지만 세션 데이터를 저장하거나 공유할 때 직렬화를 사용한다.
세션 데이터를 데이터베이스에 저장할 때나 세션 클러스터링을 통해 여러대의 서버에 세션 데이터 공유할 때 직렬화를 사용한다.
캐시(Cache)
데이터베이스로부터 조회한 객체 데이터를 다른 모듈에서도 필요할 때 DB에서 다시 조회하는 것이 아닌 객체를 직렬화하여 메모리나 외부 파일에 저장했다가 역직렬화하여 사용하는 캐시데이터로 이용할 수 있다.(Ehcache, Redis, Memcached...)
자바 RMI(Remote Method Invocation)
자바 RMI는 원격 시스템간의 메세지 교환을 위해 사용하는 자바에서 지원하는 기술이다.
이 메세지에 객체 데이터를 직렬화하여 송신하는 기술인데 최근에는 소켓을 이용하기 때문에 쓰이지 않는다.
1997년 자바에 처음 직렬화가 도입되었다.
프로그래머가 어렵지 않게 분산 객체를 만들 수 있다는 구호는 매력적이었지만, 보이지 않는 생성자, API와 구현 사이의 모호해진 경계, 잠재적인 정확성 문제, 성능, 보안, 유지보수성 등 문제가 많이 발생할 수 있다.
대부분의 값 클래스에서는 Serializable을 구현하고 있다.
직렬화의 문제점
- 공격 범위가 너무 넓고 지속적으로 더 넓어져 방어하기 어렵다
- ObjectInputStream의 readObject 메서드를 호출하여 객체 역직렬화시 Serializable 인터페이스를 구현한 클래스패스 안의 거의 모든 타입의 객체를 만들어내는 생성자와 같다. 바이트 스트림을 역직렬화하는 과정에서 이 메서드는 그 타입들 안의 모든 코드를 수행할 수 있게되므로 그 타입들의 코드 전체가 공격 범위에 들어간다.
- 자바의 표준 라이브러리나 아파치 커머즈 컬렉션 같은 서드파티 라이브러리는 물론 애플리케이션 자신의 클래스들도 공격 범위에 포함된다.
- 관련된 모든 모범 사례를 따르고 모든 직렬화 가능 클래스들을 공격에 대비하도록 작성한다 해도, 우리들의 애플리케이션은 여전히 취약할 수 있다.
- 자바 라이브러리와 서드파티 라이브러리에서 직렬화 가능 타입들을 연구하여 역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 메서드를 가젯(gadget)이라 부른다.
- 여러 가젯을 함께 사용하여 가젯 체인을 구성할 수도 있는데 가끔씩 공격자가 기반 하드웨어의 네이티브 코드를 마음대로 실행할 수 있는 아주 강력한 가젯 체인도 발견되곤한다. 그래서 아주 신중하게 제작한 바이트 스트림만 역직렬화 해야 한다.
- 역직렬화에 시간이 오래 걸리는 짧은 스트림을 역직렬화하는 것만으로도 서비스 거부 공격에 쉽게 노출될 수 있다. 이런 스트림을 역직렬화 폭탄(deserialization bomb)이라고 한다.
이 객체의 그래프는 201개의 HashSet 인스턴스로 구성되며, 그 각각은 3개 이하의 객체 참조를 갖는다.
스트림의 전체 크기는 5744 바이트지만, 역직렬화는 태양이 불타 식을 때까지도 끝나지 않을 것이다.
문제는 HashSet 인스턴스를 역직렬화하려면 그 원소들의 해시코드를 계산한다는 데 있다. 루트 HashSet에 담긴 두 원소는 각각 다른 HashSet 2개씩 원소로 갖는 HashSet이다. 그리고 반복문에 의해 이 구조가 깊이 100단계까지 만들어져 이 HashSet을 역직렬화하려면 HashCode 메서드를 2100번 넘게 호출해야 한다.
문제 대처 방법
- 신뢰할 수 없는 바이트 스트림을 역직렬화하는 일 자체가 스스로 공격에 노출하는 행위이다. 따라서 직렬화 위험을 회피하는 가장 좋은 방법은 아무것도 역직렬화하지 않는 것이다.
- 새로운 시스템에서 자바 직렬화를 써야 할 이유는 전혀 없다. 객체와 바이트 시퀀스를 변환해주는 다른 매커니즘이 많이 있다.
- 크로스-플랫폼 구조화된 데이터 표현(cross-platform structured-data representation)
- 자바 직렬화의 여러 위험을 회피하면서 다양한 플랫폼 지원, 우수한 성능, 풍부한 지원도구, 활발한 커뮤니티와 전문가 집단 등 수많은 이점까지 제공한다.
- 이 표현들의 공통점은 자바 직렬화보다 훨씬 간단하고 임의 객체 그래프를 자동으로 직렬화/역직렬화 하지 않는 대신 속성-값 쌍의 집합으로 구성된 간단하고 구조화된 데이터 객체를 사용한다.
- 기본 타입 몇 개와 배열타입만 지원할 뿐이다. 이런 간단한 추상화만으로도 아주 강력한 분산 시스템을 구축하기에 충분하고 자가 직렬화가 가져온 심각한 문제들을 회피할 수 있음이 밝혀졌다.
- JSON: 브라우저와 서버의 통신용으로 설계했고 텍스트 기반이라 사람이 읽을 수 있고 오직 key-value 형태의 데이터를 표현하는데 쓰인다.
- 프로토콜 버퍼: 구글이 서버 사이에 데이터를 교환하고 저장하기 위해 설계. 이진 표현이라 효율이 훨씬 높고 문서를 위한 스키마를 제공하고 올바로 쓰도록 강요한다. 사람이 읽을 수 있는 텍스트 표현도 지원한다.
- 레거시 시스템 때문에 자바 직렬화를 완전 배제할 수 없을 때 차선책은 신뢰할 수 없는 데이터는 절대 역직렬화하지 않는 것이다.
- 직렬화를 피할 수 없고 역직렬화한 데이터가 안전한지 완전히 확신할 수 없다면 객체 역직렬화 필터링(java.io.ObjectInputFilter)을 사용
- 데이터 스트림이 역직렬화되기 전에 필터를 설치하는 기능
- '기본 수용' 모드에서는 블랙리스트에 기록된 잠재적으로 위험한 클래스를 거부
- '기본 거부' 모드에서는 화이트리스트에 기록된 안전하다고 알려진 클래스들만 수용
- 블랙리스트 방식보다 화이트 리스트 방식을 추천한다. 블랙리스트 방식은 이미 알려진 위험으로부터만 보호할 수 있기 때문이다.
- 필터링 기능은 메모리를 과하게 사용하거나 객체 그래프가 너무 깊어지는 사태로부터 보호해주지만 앞서 보여준 직렬화 폭탄은 걸러내지 못한다.
핵심정리
직렬화는 위험하니 피해야 한다. 시스템을 밑바닥부터 설계한다면 JSON이나 프로토콜버퍼 같은 대안을 사용하자. 신뢰할 수 없는 데이터는 역직렬화하지 말자. 꼭 해야한다면 객체 역직렬화 필터링을 사용하되, 이마저도 모든 공격을 막아줄 수는 없음을 기억하자. 클래스가 직렬화를 지원하도록 만들지 말고, 꼭 그렇게 만들어야 한다면 정말 신경써서 작성해야 한다.