Home Item85 (자바 직렬화의 대안을 찾으라) + JPA에서의 직렬화에 관하여
Post
Cancel

Item85 (자바 직렬화의 대안을 찾으라) + JPA에서의 직렬화에 관하여

이번 아이템부터는 직렬화에 대해 다룬다. 직렬화는 자바가 객체를 바이트 스트림으로 인코딩하고 그 바이트 스트림으로부터 다시 객체를 재구성하는 메커니즘을 말한다.

직렬화된 객체는 다른 VM에 전송하거나 디스크에 저장한 후 나중에 역직렬화할 수 있다.

이 직렬화는 위험성이 존재하는데 그 위험을 최소화하는 방법들을 다룬다.

자바 직렬화의 대안을 찾으라

1997년에 Java에 직렬화가 도입되었다.

직렬화는 프로그래머가 어렵지 않게 분산 객체를 만들 수 있다는 개념하에 매력적으로 다가왔다.

하지만 아래 위험성들이 존재했다.

  • 보이지 않는 생성자.
  • API와 구현 사이의 모호해진 경계
  • 잠재적인 정확성 문제
  • 성능, 보안, 유지보수성 문제

직렬화의 문제는 근본적으로 공격 범위가 넓고 그 범위조차 점점 넓어지고 있어 공격을 방어하기 어렵다는 점이다.

직렬화의 보안 문제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private final Object readObject(Class<?> type)  
    throws IOException, ClassNotFoundException  
{  
	if (enableOverride) {  
		return readObjectOverride();  
	}  
	  
	if (! (type == Object.class || type == String.class))  
		throw new AssertionError("internal error");  
	  
	// if nested read, passHandle contains handle of enclosing object  
	int outerHandle = passHandle;  
	try {  
		Object obj = readObject0(type, false);  
		handles.markDependency(outerHandle, passHandle);  
		ClassNotFoundException ex = handles.lookupException(passHandle);  
		if (ex != null) {  
			throw ex;  
		}  
		if (depth == 0) {  
			vlist.doCallbacks();  
			freeze();  
		}  
		return obj;  
	} finally {  
		passHandle = outerHandle;  
		if (closed && depth == 0) {  
			clear();  
		}  	
	}  
}
  • ObjectInputStream의 readObject() 메서드이다.
  • 이를 보이지 않는 생성자라고 표현한다.
    • 직렬화된 객체를 역직렬화할 때 객체의 생성이 readObject()를 통해 이루어지기 때문이다.
    • 직렬화된 바이트 스트림으로부터 객체를 재구성한다.
    • 명시적인 생성자 호출 없이 객체가 생성된다. 객체는 복원되지만 일반적인 생성자를 사용하지 않는다.

readObject() 메서드는 직렬화를 위해 Serializable을 구현했다면 클래스패스 안의 거의 모든 타입의 객체를 만들어낼 수 있는 마법같은 생성자이다.

문제는 바이트 스트림을 역직렬화하는 과정에서 이 메서드는 그 타입 안의 모든 코드를 수행할 수 있는데 그 코드 전체가 공격 대상이 된다는 뜻이다.

자바의 표준 라이브러리나 아파치 커먼즈 컬렉션 같은 서드파티 라이브러리는 물론 애플리케이션 자신의 클래스들도 공격 범위에 포함된다.

이를 방지하기 위해 관련 모범 사례를 따르고 모든 직렬화 가능 클래스들을 공격에 대비하도록 작성해도 취약할 수 있다.

1
2
3
4
5
6
자바의 역직렬화는 명백하고 현존하는 위험이다. 이 기술은
직접, 혹은 자바 하부 시스템, JMX, JMS을 통해 간접적으로 쓰이고 있기 때문이다.

신뢰할 수 없는 스트림을 역직렬화하면 원격 코드 실행, 서비스 거부 등의
공격으로 이어질 수 있다.
잘못한 것이 아무것도 없는 애플리케이션이라도 이런 공격에 취약해질 수 있다.

CERT 조정 센터의 기술 관리자인 로버트 시커드의 말이다. 그만큼 역직렬화 과정에서의 위험이 크다는 것이다.

가젯

전문가들은 직렬화 가능 타입들을 연구해 역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 메서드들을 찾아보았다.

이 메서드들을 가젯이라고 부른다.

여러 가젯을 함께 사용해 가젯 체인을 구성할 수도 있다. 가끔 공격자가 기반 하드웨어의 네이티브 코드를 마음대로 실행할 수 있는 아주 강력한 가젯 체인도 발견된다.

따라서 아주 신중하게 제작한 바이트 스트림만 역직렬화의 대상이 되어야 한다.

역직렬화 폭탄

가젯이 아니더라도 역직렬화에 시간이 오래 걸리는 짧은 스트림을 역직렬화하는 것만으로도 서비스 거부 공격에 쉽게 노출될 수 있다.

이러한 스트림을 역직렬화 폭탄이라고 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static byte[] bomb() {
	Set<Object> root = new HashSet<>();
	Set<Obejct> s1 = root;
	Set<Obejct> s2 = new HashSet<>();

	for (int i = 0; i < 100; i++) {
		Set<Obejct> t1 = new HashSet<>();
		Set<Obejct> 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);
}
  • 이 스트림의 역직렬화는 영원히 계속된다.
    • HashSet 인스턴스를 역직렬화하려면 그 원소들의 해시코드를 계산해야 하기 때문이다.
  • root HashSet에 담긴 두 원소는 각각 다른 HashSet 2개씩을 원소로 갖는 HashSet이다.
  • 그리고 root에 담긴 이 HashSet 또한 다른 HashSet 2개씩을 원소로 갖는다.
  • 이를 100번 반복해 같은 구조의 깊이 100단계까지의 HashSet이 만들어진다.
    • 이 HashSet을 역직렬화하기 위해서는 hashCode 메서드를 2^100번 넘게 호출해야 하는 것이다.

이렇게 역직렬화가 영원히 계속되는 문제도 크지만, 잘못되었다는 신호조차 주지 않는 것이 더 큰 문제이다.

역직렬화 문제 대응법

애초에 신뢰할 수 없는 바이트 스트림을 역직렬화하는 일 자체가 스스로를 공격에 노출하는 행위이다.

따라서 직렬화 위험을 회피하는 가장 좋은 방법은 아무것도 역직렬화하지 않는 것이다.

우리가 작성하는 새로운 시스템에서 자바의 직렬화를 써야 할 이유는 전혀 없다.

객체와 바이트 시퀀스를 변환해주는 다른 메커니즘이 많이 있다.

이 방식들은 자바 직렬화의 여러 위험을 회피하면서 다양한 플랫폼 지원, 우수한 성능, 풍부한 지원 도구, 활발한 커뮤니티 등 수많은 이점이 존재한다.

  • JSON
    • 브라우저와 서버의 통신용으로 설계되었다.
    • 사람이 읽을 수 있다.
    • 오직 데이터를 표현하는데 쓰인다.
  • 프로토콜 버퍼
    • 구글이 서버 사이에 데이터를 교환하고 저장하기 위해 설계했다.
    • 이진표현이기 때문에 효율이 높다.
    • 문서를 위한 스키마를 제공하고 올바로 쓰도록 강요한다.

근데 JPA에서 복합키 구성할 때 Serializable 구현해야 하지 않나?

결론부터 말하면 JPA에서 Serializable 구현은 자바 직렬화의 보안 문제와는 관계가 없다.

JPA에서 직렬화를 구현하는 것은 객체를 직접 바이트 스트림으로 변환하거나 네트워크를 통해 전송하는 용도로 사용되지 않는다.

복합 키 객체의 JPA 엔티티 관리 메커니즘과의 호환성때문에 직렬화가 필요한 것이다.

즉, 자바의 기본 직렬화 기능을 사용하는 것이 아니라고 볼 수 있고, 직렬화 보안 문제와는 직접적인 관련이 없는 것이다.

정리

자바의 직렬화는 매우 위험하다. 그냥 사용하지 않는 것을 권한다.

새로운 시스템에서 자바 직렬화를 사용할 일은 절대 없다. JSON, 프로토콜 버퍼와 같은 안전한 대안이 존재한다.

만약 레거시에 직렬화가 존재한다면 정말 믿을만한 데이터만 역직렬화해야 한다.

역직렬화 필터 등의 방어 방법은 있지만 완전하게 안전을 보장하지는 않는다.

시간과 노력을 들여 크로스-플랫폼 구조화된 데이터 표현으로 마이그레이션하는 것을 고민해야할 정도로 자바의 직렬화는 안전하지 않다.

This post is licensed under CC BY 4.0 by the author.

Item84 (프로그램의 동작을 쓰레드 스케줄러에 기대지 말라) + 스프링 스케줄러

Item86 (Serializable을 구현할지는 신중히 결정하라)