Serialization
자바 내부 시스템에서 사용되는 객체나 데이터를 외부에서 사용할 수 있도록 Byte 형태로 변환
- Java.io.Serializable 인터페이스를 상속받은 객체와 Primitive 타입의 데이터가 직렬화의 대상
- 기본자료형(Primitive Type)은 정해진 Byte의 변수이기 때문에 Byte 단위로 변환 가능
- 객체의 크기는 가변적이며, 객체를 구성하는 자료형들의 종류와 수에 따라 객체의 크기가 다양하게 바뀔 수 있기 때문에 객체를 직렬화하기 위해 Serializable 인터페이스를 구현
- 객체의 멤버들 중 Serializable 인터페이스가 구현되지 않은 것이 존재하면 안 됨
- Transient가 선언된 멤버는 전송되지 않음
- 객체 내에 Serializable 인터페이스가 구현되지 않은 멤버 때문에 NonSerializableException이 발생하는 경우, Transient를 선언해주면 직렬화 대상에서 제외되기 때문에 문제없이 해당 객체를 직렬화 가능.
자바 직렬화의 장점
- 자바 직렬화는 자바 시스템에서 개발에 최적화되어 있음.
- 복잡한 데이터 구조의 클래스의 객체라도 직렬화 기본 조건만 지키면 큰 작업 없이 바로 직렬화를 가능하며 역직렬화도 가능!
당연하게 보이는 장점 중에 하나지만 데이터 타입이 자동으로 맞춰지기 때문에 관련 부분을 큰 신경을 쓰지 않아도 됨! 그렇게 역직렬화가 되면 기존 객체처럼 바로 사용 가능.
참고 : 자바 RMI(Remote Method Invocation)
- 직렬화 된 Java 클래스의 전송, 원격 프로시저 호출(RPC)과 같은 객체 지향적인 원격 메서드 호출을 수행하는 Java API
- 기존 자바 언어의 장점과 풍부한 API를 분산 객체 기술에 이용이 가능하게 됨
자바 직렬화를 지양해야하는 이유
우아한형제들
직렬화는 위험하다.
직렬화의 위험성을 회피하는 가장 좋은 방법은 아무것도 역직렬화하지 않는 것이다.
자바 직렬화는 왜 위험한가?
- 공격 범위가 너무 넓음
- 지속적으로 더 넓어져 방어하기도 어려움
- OutputInputStream의 readObject 메서드를 호출하면서 객체 그래프가 역직렬화(deserialization)됨
- readObject 메서드는 (Serializable 인터페이스를 구현했다면) 클래스패스 안의 거의 모든 타입의 객체를 만들어낼 수 있는 사실상 마법같은 생성자.
- 바이트 스트림을 역직렬화하는 과정에서 readObject 메서드는 그 타입들 안의 모든 코드를 수행 가능
- 즉, 그 타입들의 코드 전체가 악의적인 공격 범위에 들어갈 수 있음!
- 자바의 표준 라이브러리나 서드파티 라이브러리, 그리고 어플리케이션 자신의 클래스들도 공격 범위에 포함됨
자바의 역직렬화는 명백하고 현존하는 위험이다.
자바 하부 시스템 RMI(Remote Method Invocation), JMX(Java Management Extension), JMS(Java Messaging System)
을 통해 간접적으로 쓰이고 있기 때문이다.
신뢰할 수 없는 스트림을 역직렬화하면 원격 코드 실행(remote code execution, RCE), 서비스 거부(denial-of-service, Dos) 등이 공격으로 이어질 수 있다. 잘못한 게 아무것도 없는 애플리케이션이라도 이런 공격에 취약해질 수 있다.
- 역직렬화 과정에서 호출되어 잠재적인 위험한 동작을 수행하는 메서드를 가젯(gadget) 이라고 한다.
- 하나의 가젯이 또는 여러 개의 가젯이 마음대로 코드를 수행하게 할 수 있기 때문에 아주 신중하게 제작된 바이트 스트림만 역직렬화해야 한다.
역직렬화 폭탄 예시 - 이 스트림의 역직렬화는 영원히 계속된다!
- 역직렬화에 시간이 오래 걸리는 짧은 스트림을 역직렬화 폭탄(deserialization bomb)이라고 함.
- 역직렬화에 시간이 오래 걸리는 짧은 스트림을 역직렬화하는 것만으로도 서비스 거부 공격에 쉽게 노출될 수 있다.
- 아래 코드는 HashSet과 문자열을 이용해 역직렬화 폭탄을 테스트 한 예시
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개 이하의 객체 참조를 가지며 스트림의 전체 크기는 5744바이트.
- 그러나 역직렬화는 끝나지 않음.
- 문제는 HashSet 인스턴스를 역직렬화하려면 그 원소들의 해시코드를 계산해야 한다는 데 있다. 역직렬화과 영원히 계속된다는 것도 문제지만, 무언가 잘못되었다는 신호조차 주지 않는다는 것도 큰 문제이다.
- 단 몇 개의 객체만 생성해도 스택 깊이 제한에 걸려버린다.
해결법
-
신뢰할 수 없는 바이트 스트림을 역직렬화하는 일 자체가 스스로를 공격에 노출하는 행위이다.
-
직렬화 위험을 회피하는 가장 좋은 방법은 아무것도 역직렬화하지 않는 것이다.
-
우리가 작성하는 새로운 시스템에서 자바 직렬화를 써야 할 이유는 전혀 없다.
-
객체와 바이트 시퀀스를 변환해주는 다른 메커니즘이 많이 있다.
-
자바 직렬화의 여러 위험을 회피하면서 다양한 플랫폼 지원, 우수한 성능, 풍부한 지원 도구, 활발한 커뮤니티와 전문가 집단 등 수많은 이점까지 제공한다.
- 자바 직렬화보다 훨씬 더 간단!
- 임의 객체 그래프를 자동으로 직렬화 및 역직렬화 하지 않음
- 대신 속성-값 쌍의 집합으로 구성된 간단하고 구조화된 데이터 객체를 사용
- 기본 타입 몇 개와 배열 타입만 지원
크로스 플랫폼 구조화된 데이터 표현은 JSON과 프로토콜 버퍼가 있다.
- JSON은 더글라스 크록퍼드가 브라우저와 서버의 통신용으로 설계했고, 프로토콜 버퍼는 구글이 서버 사이에 데이터를 교환하고 저장하기 위해 설계했다.
- JSON은 자바스크립트용, 프로토콜 버퍼는 C++용이다.
JSON과 프로토콜 버퍼의 차이
-
JSON은 텍스트 기반이라 사람이 읽을 수 있고, 프로토콜 버퍼는 이진 표현이라 효율이 훨씬 높다.
-
JSON은 오직 데이터를 표현하는 데만 쓰이지만, 프로토콜 버퍼는 문서를 위한 스키마를 제공하고 올바로 쓰도록 강요한다.
-
효율은 프로토콜 버퍼가 훨씬 좋지만 텍스트 기반 표현에는 JSON이 아주 효과적이다.
-
자바 직렬화는 크로스-플랫폼 구조화된 데이터 표현 방법으로 대체해야 한다.
-
예) JSON, protocol buffer 등이 있다. 프로토콜 버퍼는 이진 표현이라 효율이 훨씬 높으며 JSON은 텍스트 기반이라 사람이 읽을 수 있는 장점이 있다.
어쩔 수 없이 직렬화를 사용해야 할 때
- 레거시 시스템 때문에 직렬화를 배제할 수 없을 때의 차선책은 신뢰할 수 없는 데이터는 절대 역직렬화하지 않는 것이다.
- 신뢰할 수 없는 발신원으로부터의 RMI는 절대 수용해서는 안된다.
직렬화를 피할 수 없고 역직렬화한 데이터가 안전한지 확신할 수 없다면 객체 역직렬화 필터링(java.io.ObjectInputFilter)을 사용하자(자바 9)
객체 역직렬화 필터링
객체 역직렬화 필터링은 데이터 스트림이 역직렬화되기 전에 필터를 설치하는 기능이다.
- 클래스 단위로, 특정 클래스를 받아들이거나 거부할 수 있다.
- 기본 수용 모드에서는 블랙리스트에 기록된 잠재적으로 위험한 클래스들을 거부한다.
- 기본 거부 모드에서는 화이트리스트에 기록된 안전하다고 알려진 클래스들만 수용한다.
- 블랙리스트 방식 보다는 화이트리스트 방식을 사용할 것!
- 화이트리스트를 자동으로 생성해주는 스왓(SWAT, Serial Whitelist Application Trainer)이라는 도구도 있다.
- 필터링 기능은 메모리를 과하게 사용하거나 객체 그래프가 너무 깊어지는 사태로부터 보호해준다. 그러나 직렬화 폭탄은 걸러내지 못한다.
- 자바 직렬화를 사용하는 시스템 관리 시 크로스 플랫폼 구조화된 데이터 표현으로 마이그레이션하는 것을 추천한다.
정리
- 직렬화는 위험하니 피해야 한다.
- JSON이나 프로토콜 버퍼 같은 대안을 사용하자.
- 신뢰할 수 없는 데이터는 역직렬화하지 말자.
- 꼭 해야 한다면 객체 역직렬화 필터링을 사용하되, 이마저도 사실 모든 공격을 막아줄수는 없다는 것을 꼭 알고 있자.