직렬화

직렬화는 프로그래머가 어렵지 않게 분산 객체를 만들 수 있다는 매력이 있지만, 보이지 않는 생성자, API와 구현 사이 모호해진 경계, 잠재적 정확성 문제, 성능, 보안, 유지보수성 등 대가가 크다는 단점이 있다.

더 근본적으로는 공격당한 포인트가 너무 많다는 것이 있다. Object의 readObject(implement Serializable)는 classPath 안 거의 모든 타입의 객체를 만들어 낼 수 있는 마스터키와 같다. 바이트 스트림을 역직렬화하는 과정에서 이 메소드는 타입들 안의 모든 코드를 수행할 수 있게 된다. 실제로 신뢰할 수 없는 스트림을 역직렬화하면 원격 코드 실행(RCE), 서비스 거부(denial-of-service, DoS) 등의 공격을 유발할 수 있다.

직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 메소드들을 gadget이라고 부른다. gadget끼리 체이닝을 구서알 수도 있다. 이런 체이닝으로 하드웨어 네이티브 코드를 마음대로 실행할 수 있는 강력한 취약점이 발견되고는 한다.(RCE)

혹은 역직렬화에 시간이 오래 걸리게 해서 deserialization bomb을 투하할 수도 있다. (DoS) 이는 역직렬화 과정에서 그래프 탐색을 해서 계속 역직렬화 하는 것에서 착안된 공격이다. 이를 피하기 위해서 그래프 탐색을 멈추는 방법이 있다.

이런 단점이 있는데 굳이 (역)직렬화를 할 필요는 없다. JSON/ protocolBuffer가 대안으로 있다. 둘의 차이는 사람이 그냥 눈으로 읽고 이해할 수 있는가 정도다. JSON은 사람이 읽을 수 있다. protocolBuffer는 바이너리 표현이라 어렵다. 물론 사람이 읽을 수 있도록 개선한 protocolBuffer도 있다(pbtxt)

뭐 이래도 굳이 (역)직렬화를 해야한다면 java.io.ObjectInputFilter로 스트림이 역직렬화되기 전에 필터링할 수 있다. 화이트리스트 정책을 잡아서 필터링하고 그 결과를 (역)직렬화하면 비교저거 안전하다.

Serializable을 구현할지는 고민해봐라

늘 그렇듯 인터페이스 구현 문제다. serializable을 구현하면 serialVersionUID가 생기는데 내부 구현을 바꾸면 이 값이 달라져 원래 직렬화 형태와 달라지게 된다. (그래서 직접 고른 값으로 명시적으로 작성하여 해당 문제를 피하기도 한다.) 이러면 더 이상 확장을 불가능하게 한다. 물론 그대로 두고 내부 구현을 바꿀 수도 있지만 소스코드에 불필요한 혹을 남겨놓게 된다.

또한 Serializable은 언어의 기본 생성 메커니즘을 뒤흔든다. 생성자 없이 객체 생성을 할 수 있게 하기 때문이다. 이 과정에서 일부러 생성을 실패하게 하고 finalize를 실행시키는 공격도 가능하다.

굳이 쓴다면 커스터마이즈하자

고민해보고 기본 직렬화를 사용해도 될거 같으면 사용해도 된다. 직렬화하면 객체를 루트로하는 객체의 데이터와 그 객체에서부터 접근할 수 있는 모든 객체를 담아내며, 객체들이 연결된 토폴로지까지 기술한다. 이렇게 꽤나 자세한 정보들이 기술되어 있기 때문에 불변식 보장과 보안을 위해서 readObject를 커스터마이징해서 제공해야 할 때가 많다. 또한, 객체의 물리적 표현과 논리적 표현 차이가 클 때 기본 직렬화 형태를 사용하는 것이 문제가 생기는 경우가 있다.

  1. 공개 API가 내부 구현방식에 종속된다.
  2. 너무 많은 공간을 차지할 수도 있다.
  3. 시간이 오래걸릴 수도 있다.
  4. 스택오버플로의 위험에 노출된다.

또한 기본 직렬화를 사용하면 @Transient 값이 직렬화/ 역직렬화 대상에서 제외되므로 문제가 될 수도 있다.