85. 자바 직렬화의 대안을 찾으라
•
1997년 자바에 처음으로 직렬화가 도입되었다.
•
프로그래머가 어렵지 않게 분산 객체를 만들 수 있다는 구호는 매력적이지만, 보이지 않는 생성자, API와 구현 사이의 모호해진 경계, 잠재적인 정확성 문제, 성능, 보안, 유지보수성 등 그 대가가 컷다.
자바 직렬화는 위험하다.
•
직렬화의 근본적인 문제는 공격 범위가 너무 넓고 지속적으로 더 넓어져 방어하기 어렵다는 점이다.
•
ObjectInputStream의 readObject 메서드를 호출하면서 객체 그래프가 역직렬화되기 때문이다.
•
readObject 메서드는 Serializable 인터페이스를 구현했다면 클래스패스 안의 거의 모든 타입의 객체를 만들어낼 수 있는 마법 같은 생성자다.
•
바이트 스트림을 역직렬화하는 과정에서 이 메서드는 그 타입들 안의 모든 코드를 수행할 수 있다.
◦
즉, 그 타입들의 코드 전체가 공격 범위 들어간다는 뜻이다.
•
직렬화의 모범 사례를 따르고 모든 직렬화 가능 클래스를 공격에 대비해도 여전히 취약할 수 있다.
가젯 (gadget)
•
공격자와 보안 전문가들은 라이브러리나 서드파티의 역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 메서드를 찾아 가젯이라 부른다.
•
여러 가젯을 함께 사용하여 가젯 체인을 구성하면, 가끔씩 공격자가 기반 하드웨어의 네이티브 코드를 마음대로 실행할 수 있는 아주 강력한 가젯 체인도 발견되곤 한다.
역직렬화 폭탄 (Deserialization bomb)
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);
}
Java
복사
•
시간이 오래 걸리는 짧은 스트림을 역직렬화하는 것만으로도 서비스 거부 공격에 쉽게 노출될 수 있다.
•
스트림 전체의 크기는 5744바이트지만, 역직렬화는 영원히 끝나지 않을 것이다.
•
HashSet에 지속적으로 자식을 더해 100개까지 증가시킨다.
◦
이 구조를 직렬화 하려면 hashCode() 메서드를 번 넘게 호출해야 한다.
•
가장 큰 문제점은 무언가 잘못되었다는 신호조차 주지 않는다는 것이다.
직렬화 문제를 피하는 방법
•
자바 직렬화 위험을 회피하는 가장 좋은 방법은 역직렬화 하지 않는 것이다.
•
우리들이 작성하는 새로운 시스템에 자바 직렬화를 써야 할 이유가 전혀 없다.
◦
객체와 바이트 시퀀스를 변환해주는 다른 메커니즘이 많이 있기 때문이다.
크로스-플랫폼 구조화된 데이터 표현
•
직렬화의 여러 위험을 회피하며, 다양한 플랫폼 지원, 우수한 성능, 풍부한 지원 도구 등을 제공하는 다른 방식의 매커니즘 방식을 크로스-플랫폼 구조화된 데이터 표현이라 한다.
•
대표적으로 JSON과 protobuf가 있다.
장점
•
자바의 직렬화보다 훨씬 간단하다.
•
임의의 객체 그래프를 자동으로 직렬화/역직렬화 하지 않는다.
•
속성-값 쌍의 집합으로 구성된 간단하고 구조화된 데이터 객체를 사용한다.
•
간단한 추상화로 아주 강력한 분산 시스템을 구축하고, 자바 직렬화의 문제점을 회피할 수 있다.
객체 역직렬화 필터링 (ObjectInputFilter)
•
레거시 때문에 자바 직렬화를 배제할 수 없을 때의 차선책은 신뢰할 수 없는 데이터는 절대 역직렬화하지 않는 것이다.
•
만약 직렬화를 피할 수 없고 역직렬화한 데이터가 안전한지 확신할 수 없다면 객체 역직렬화 필터링(java.io.ObjectInputFilter)을 사용하자.
◦
이 기능은 데이터 스트림이 역직렬화되기 전에 필터를 설치하는 기능이다.
•
블랙리스트에 기록된 클래스를 거부하거나, 화이트리스트에 기록된 클래스만 수용한다.
◦
이미 알려진 위험으로부터만 보호할 수 있는 블랙리스트보다 화이트리스트를 사용하는 것이 좋다.
86. Serializable을 구현할지는 신중히 결정하라
•
어떤 클래스의 인스턴스를 직렬화할 수 있게 하려면 클래스 선언에 implements Serializable을 덧붙이면 된다.
◦
직렬화를 지원하기란 짧게 보면 손쉬워 보이지만, 길게 보면 아주 값비싼 일이다.
문제점들
Serializable을 구현하면 릴리스한 뒤에는 수정하기 어렵다.
•
클래스가 Serializable을 구현하면 직렬화된 바이트 스트림 인코딩(직렬화 형태)도 하나의 공개 API가 된다.
•
그래서 이 클래스가 널리 퍼진다면 그 직렬화 형태도 (다른 공개 API와 마찬가지로) 영원히 지원해야 하는 것이다.
•
필드로의 접근을 막기 어려워 정보 은닉 또한 어려워진다.
•
뒤늦게 클래스를 손보면 구/신 버전의 인스턴스 역직렬화시 실패할 수 있다.
•
직렬화 형태를 잘 살계하더라도 클래스를 개선하는 데 제약이 될 수 있다.
•
모든 직렬화된 클래스는 고유 식별 번호(serialVersionUID)를 부여받는다.
◦
이를 자동 생성되는 값에 의존하면 쉽게 호환성이 깨져버려 InvalidClassException이 발생할 수 있다.
버그와 보안 구멍이 생길 위험이 높아진다.
•
객체는 생성자를 사용해 만드는 게 기본이다.
◦
즉, 직렬화는 언어의 기본 메커니즘을 우회하는 객체 생성 기법인 것이다.
•
기본 방식을 따르든 재정의해 사용하든, 역직렬화는 일반 생성자의 문제가 그대로 적용되는 '숨은 생성자'다.
해당 클래스의 신버전을 릴리스할 때 테스트할 것이 늘어난다.
•
직렬화 가능 클래스가 수정되면 신버전 인스턴스를 직렬화한 후 구버전으로 역직렬화할 수 있는지, 그리고 그 반대도 가능한지를 검사해야 한다.
•
따라서 테스트해야 할 양이 직렬화 가능 클래스의 수와 릴리스 횟수에 비례해 증가한다.
•
양방향 직렬화/역직렬화가 모두 성공하고, 원래의 객체를 충실히 복제해내는지를 반드시 확인해야 한다.
추가적인 조언
Serializable 구현 여부는 가볍게 결정할 사안이 아니다.
•
객체를 전송하거나 저장할 때 자바 직렬화를 이용하는 프레임워크용으로 만든 클래스라면 선택의 여지가 없다.
상속용으로 설계된 클래스
•
상속용으로 설계된 클래스는 대부분 Serializable을 구현하면 안 되며, 인터페이스도 대부분 Serializable을 확장해서는 안 된다.
내부 클래스는 직렬화를 구현하지 말아야 한다.
•
단, 정적 멤버 클래스는 구현해도 된다.
87. 커스텀 직렬화 형태를 고려해보라
고려하기
•
먼저 고민해보고 괜찮다고 판단될 때만 기본 직렬화 형태를 사용하라.
•
객체의 물리적 표현과 논리적 내용이 같다면 기본 직렬화 형태라도 좋다.
•
기본 직렬화 형태가 적합하다고 결정했더라도 불변식 보장과 보안을 위해 readObject 메서드를 제공해야 할 때가 많다.
이상적인 직렬화 클래스
public class Name implements Serializable {
/**
* 성. null이 아니어야 한다.
* @serial
*/
private final Stirng lastName;
/**
* 이름. null이 아니어야 한다.
* @serial
*/
private final String firstName;
/**
* 중간이름. 중간이름이 없다면 null
* @serial
*/
private final String middleName;
... // 나머지 코드는 생략
}
Java
복사
기본 직렬화 형태에 적합하지 않은 클래스
public final class StringList implements Serializable {
private int size = 0;
private Entry head = null;
private static class Entry implements Serializable {
String data;
Entry next;
Entry previous;
}
}
Java
복사
직렬화의 문제 4가지
•
공개 API가 현재의 내부 표현 방식에 영구히 묶인다.
•
너무 많은 공간을 차지할 수 있다.
•
시간이 너무 많이 걸릴 수 있다.
•
스택오버플로를 일으킬 수 있다.
커스텀 직렬화
public final class StringList implements Serializable {
private transient int size = 0;
private transient Entry head = null;
// 이제는 직렬화되지 않는다.
private static class Entry {
String data;
Entry next;
Entry previous;
}
// 지정한 문자열을 이 리스트에 추가한다.
public final void add(String s) {...}
/**
* 이 {@code StringList} 인스턴스를 직렬화한다.
*
* @serialData 이 리스트의 크기(포함된 문자열의 개수)를 기록한 후
* ({@code int}), 이어서 모든 원소를(각각은 {@code String})
* 순서대로 기록한다.
*/
private void writeObject(ObjectOutputStream s) throws IOException {
//기본 직렬화를 수행한다.
s.defaultWriteObject();
s.writeInt(size);
// 커스텀 역직렬화를 수행한다.
// 모든 원소를 올바른 순서로 기록한다.
for (Entry e = head; e != null; e = e.next)
s.writeObject(e.data);
}
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
//기본 역직렬화를 수행한다.
s.defaultReadObject();
int numElements = s.readInt();
// 커스텀 역직렬화 부분
// 모든 원소를 읽어 이 리스트에 삽입한다.
for(int i = 0; i < numElements; i++) {
add((String) s.readObject());
}
}
}
Java
복사
추가적인 조언
•
어떠한 직렬화 형태를 택하든 직렬화 가능 클래스 모두에 serialVersionUID 를 명시적으로 부여하자.
•
구버전으로 직렬화된 인스턴스들과 호환성을 끊으려는것이 아니면 serialVersionUID 를 바꾸지 말자.
88. readObject 메서드는 방어적으로 작성하라
안전한 readObject 메서드를 작성하는 지침
•
private이어야 하는 객체 참조 필드는 각 필드가 가리키는 객체를 방어적으로 복사하라.
◦
불변 클래스 내의 가변 요소가 여기 속한다.
•
모든 불변식을 검사하여 어긋나는 게 발견되면 InvalidObjectException을 던진다.
◦
방어적 복사 다음에는 반드시 불변식 검사가 뒤따라야 한다.
•
역직렬화 후 객체 그래프 전체의 유효성을 검사해야 한다면 ObjectInputValidation 인터페이스를 사용하라
•
직접적이든, 간접적이든, 재정의할 수 있는 메서드는 호출하지 말자.
89. 인스턴스 수를 통제해야 한다는 readResolve보다는 열거타입을 사용하라
문제점
•
implements Serializable을 구현하게 되는 순간 싱글턴이 아니게 된다.
•
기본 직렬화를 쓰지 않거나 명시적인 readObject 메서드를 제공하더라도 소용이 없다.
•
어떤 readObject 메서드를 사용하더라도 초기화될 때 만들어진 인스턴스와 다른 인스턴스를 반환하게 된다.
•
이때 readResolve 메서드를 이용하면 readObject 메서드가 만든 인스턴스를 다른 것으로 대체할 수 있다.
•
이때 readObject 가 만들어낸 인스턴스는 가비지 컬렉션의 대상이 된다.
•
만일 그렇지 않으면 역직렬화(Deserialization) 과정에서 역직렬화된 인스턴스를 가져올 수 있다.
◦
즉, 싱글턴이 깨지게 된다.
해결법
•
불변식을 지키기 위해 인스턴스를 통제해야 한다면 가능한 열거 타입을 사용하자.
•
열거 타입 사용이 여의치 않은 상황에 직렬화와 인스턴스 통제가 필요하다면 readResolve 메서드를 사용하자.
•
단, 그 클래스의 모든 참조 타입 인스턴스 필드에 transient 한정자를 선언해야 한다.