자바 직렬화의 대안을 찾으라 - Effective Java[85]
🔗 자바 직렬화의 위험성
-
1997년, 자바에 처음으로 직렬화가 도입될 당시, 프로그래머가 어렵지 않게 분산 객체를 만들 수 있다는 구호는 매력적이었지만, 보이지 않는 생성자, API와 구현 사이의 모호해진 경계, 잠재적인 정확성 문제, 성능, 보안, 유지보수성 등 그 대가가 컸다.
-
보안문제는 실제로도 우려한 만큼 심각한 것으로 밝혀졌다.
-
2000년대 초반에 논의된 취약점들이 그 후로 십 년이상 심각하게 악용되었다.
-
2016년 11월에는 샌프란시스코 시영 교통국이 랜섬웨어 공격을 받아 요금 징수 시스템이 이틀간 마비되는 사태를 겪기도 했다.
-
💎 직렬화의 근본적인 문제는 공격 범위가 너무 넓고 지속적으로 더 넓어져 방어하기 어렵다는 점이다.
-
ObjectInputStream의 readObject 메서드는 (Serializable 인터페이스를 구현했다면) 클래스패스 안의 거의 모든 타입의 객체를 만들어낼 수 있는, 사실상 마법 같은 생성자다.
-
바이트 스트림을 역직렬화하는 과정에서 이 메서드는 그 타입들 안의 모든 코드를 수행할 수 있다.
-
즉, 그 타입들의 코드 전체가 공격 범위에 들어간다는 뜻이다.
-
-
자바의 표준 라이브러리나 아파치 커먼즈 컬렉션 같은 서드파티 라이브러리는 물론 애플리케이션 자신의 클래스들도 공격 범위에 포함된다.
- 관련한 모든 모범 사례를 따르고 모든 직렬화 가능 클래스들을 공격에 대비하도록 작성한다 해도, 애플리케이션은 여전히 취약할 수 있다.
-
CERT 조정 센터의 기술 관리자인 로버트 시커드는 자바 역직렬화에 대해 다음과 같이 평했다.
자바의 역직렬화는 명백하고 현존하는 위험이다.
이 기술은 지금도 애플리케이션에서 직접 혹은, 자바 하부 시스템(RMI, JMX, JMS)를 통해 간접적으로 쓰이고 있기 때문이다.
신뢰할 수 없는 스트림을 역직렬화하면 원격 코드 실행(RCE), 서비스 거부(DOS) 등의 공격으로 이어질 수 있다.
잘못한 게 아무것도 없는 애플리케이션이라도 이런 공격에 취약해질 수 있다.
💎 역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 메서드들 :: 가젯(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); // 간결하게 하기 위해 이 메서드의 코드는 생략함
}
-
이 객체의 그래프는 201개의 HashSet 인스턴스로 구성되며, 그 각각은 3개 이하의 객체 참조를 갖는다.
-
스트림의 전체 크기는 5,744바이트지만, 역직렬화는 태양이 불타 식을 때까지도 끝나지 않을 것이다.
-
문제는 HashSet 인스턴스를 역직렬화하려면 그 원소들의 해시코드를 계산해야 한다는 데 있다.
-
루트 HashSet에 담긴 두 원소는 각각 (루트와 마찬가지로) 다른 HashSet 2개씩을 원소로 갖는 HashSet이다.
-
그리고 반복문에 의해 이 구조가 깊이 100단계까지 만들어진다.
-
따라서 이 HashSet을 역직렬화하려면 hashCode 메서드를 2^100번 넘게 호출해야 한다.
-
역직렬화가 영원히 계속된다는 것도 문제지만, 무언가 잘못되었다는 신호조차 주지 않는 것도 큰 문제다.
-
이 코드는 단 몇 개의 객체만 생성해도 스택 깊이 제한에 걸려버린다.
-
-
💎 역직렬화 폭탄은 어떻게 대처해야 할까?
-
애초에 신뢰할 수 없는 바이트 스트림을 역직렬화하는 일 자체가 스스로를 공격에 노출하는 행위다.
-
따라서 직렬화 위험을 회피하는 가장 좋은 방법은 아무것도 역직렬화하지 않는 것이다.
💎 작성하는 새로운 시스템에서 자바 직렬화를 써야 할 이유는 전혀 없다.
-
객체와 바이트 시퀀스를 변환해주는 다른 메커니즘이 많이 있다.
-
이 방식들은 자바 직렬화의 여러 위험을 회피하면서 다양한 플랫폼 지원, 우수한 성능, 풍부한 지원 도구, 활발한 커뮤니티와 전문가 집단 등 수많은 이점까지 제공한다.
-
이런 메커니즘도 직렬화 시스템이라 불리기도 하지만 기존의 자바 직렬화와 구분하고자 크로스-플랫폼 구조화된 데이터 표현이라 한다.
-
이 표현들의 공통점은 자바 직렬화보다 훨씬 간단하다는 것이다.
-
임의 객체 그래프를 자동으로 직렬화/역직렬화하지 않는다.
-
대신 속성-값 쌍의 집합으로 구성된 간단하고 구조화된 데이터 객체를 사용한다.
-
그리고 기본 타입 몇 개와 배열 타입만 지원할 뿐이다.
-
이런 간단한 추상화만으로도 아주 강력한 분산 시스템을 구축하기에 충분하고, 자바 직렬화가 가져온 심각한 문제들을 회피할 수 있음이 밝혀졌다.
-
-
💎 크로스-플랫폼 구조화된 데이터 표현의 선두주자는 JSON과 프로토콜(Protocol Buffers) 버퍼다.
- JSON은 더글라스 크록퍼드가 브라우저와 서버의 통신용으로 설계했고, 포로토콜 버퍼는 구글이 서버 사이에 데이터를 교환하고 저장하기 위해 설계했다.
- 보통은 이들을 언어 중립적이라고 하지만, 사실 JSON은 자바스크립트용으로, 프로토콜 버퍼는 C++용으로 만들어졌고, 아직도 그 흔적이 남아 있다.
-
둘의 가장 큰 차이는 JSON은 텍스트 기반이라 사람이 읽을 수 있고, 프로토콜 버퍼는 이진 표현이라 효율이 훨씬 높다는 점이다.
-
또한 JSON은 오직 데이터를 표현하는 데만 쓰이지만, 프로토컬 버퍼는 문서를 위한 스키마를 제공하고 올바로 쓰도록 강요한다.
-
효율은 프로토콜 버퍼가 훨씬 좋지만 텍스트 기반 표현에는 JSON이 아주 효과적이다.
-
또한, 프로토콜 버퍼는 이진 표현뿐 아니라 사람이 읽을 수 있는 텍스트 표현도 지원한다.
-
💎 이미 만들어져 있는 시스템은 자바 직렬화를 완전히 배제할 수 없잖아
-
레거시 시스템 때문에 자바 직렬화를 완전히 배제할 수 없을 때의 차선책은 신뢰 할 수 없는 데이터는 절대 역직렬화 하지 않는 것이다.
-
특히, 신뢰할 수 없는 발신원으로부터의 RMI는 절대 수용해서는 안 된다.
-
자바의 공식 보안 코딩 지침에서는 “신뢰할 수 없는 데이터의 역직렬화는 본질적으로 위험하므로 절대로 피해야 한다”라고 조언한다.
-
💎 직렬화를 피할 수 없고 역직렬화한 데이터가 안전한지 완전히 확실할 수 없다면 객체 역직렬화 필터링 (java.io.ObjectInputFilter)을 사용하자.
-
객체 역직렬화 필터링은 데이터 스트림이 역직렬화되기 전에 필터를 설치하는 기능이다.
-
클래스 단위로, 특정 클래스를 받아들이거나 거부할 수 있다.
-
‘기본 수용’ 모드에서는 블랙리스트에 기록된 잠재적으로 위험한 클래스들을 거부한다.
-
반대로 ‘기본 거부’ 모드에서는 화이트 리스트에 기록된 안전하다고 알려진 클래스들만 수용한다.
-
블랙리스트 방식보다는 화이트리스트 방식을 추천한다.
- 블랙리스트 방식은 이미 알려진 위험으로부터만 보호할 수 있기 때문이다.
-
-
필터링 기능은 메모리를 과하게 사용하거나 객체 그래프가 너무 깊어지는 사례로부터 보호해준다.
- 하지만 앞서 보여준 직렬화 폭탄은 걸러내지 못한다.
💎 직렬화는 여전히 자바 생태계 곳곳에 쓰이고 있다.
-
자바 직렬화를 사용하는 시스템을 관리해야 한다면 시간과 노력을 들여서라도 크로스 플랫폼 구조화된 데이터 표현으로 마이그레이션하는 것을 심각하게 고민해보자.
-
하지만 현실적인 이유로 지금도 직렬화 가능 클래스를 작성하거나 유지보수해야 하는 사람이 있을 수 있다.
- 직렬화 가능 클래스를 올바르고 안전하고 효율적으로 작성하려면 상당한 주의가 필요하다.
직렬화는 위험하니 피해야 한다.
시스템을 밑바닥부터 설계한다면 JSON이나 프로토콜 버퍼 같은 대안을 사용하자.
신뢰할 수 없는 데이터는 역직렬화하지 말자.
꼭 해야 한다면 객체 역직렬화 필터링을 사용하되, 이마저도 모든 공격을 막아줄 수는 없음을 기억하자.
클래스가 직렬화를 지원하도록 만들지 말고, 꼭 그렇게 만들어야 한다면 정말 신경 써서 작성해야 한다.
참조 - 이펙티브 자바 3/E - 조슈아 블로크때
Comments