item3 에서 singleton 패턴을 설명할때 생성자를 호출하지 못하게 막는 방식으로 jvm 내에 instance 가 오직 하나만 만들어짐을 보장했다.
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {}
public void leaveTheBuilding() {
// ...
}
}
Java
복사
singleton pattern JVM 내에서 딱 하나의 인자만 갖는다
하지만 implements Serializable 을 추가하는 순간 더 이상 싱글턴이 아니다.
readObject 를 사용하든지 이 클래스가 초기화될 때 만들어진 instance 와는 별개인 instance 를 반환한다
대신 readResolve 를 사용하면 readObject 가 만들어낸 instance 를 대체할 수 있는데
대부분의 경우 이때 새로 생성된 객체의 참조는 유지하지 않고 GC 대상에 들어가게 된다.
public class Elvis implements Serializable {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {}
public void leaveTheBuilding() { ... }
// instance 통제를 위한 readReolve - 개선의 여지가 있다.
private Object readResolve() {
// 진짜 Elvis 를 반환하고, 가짜 Elvis 는 GC 에 맡긴다.
return INSTANCE;
}
}
Java
복사
readResolve 는 deserialize 된 객체는 무시하고 class 가 초기화 때 만들어진 Elvis 를 반환한다.
따라서 모든 필드를 transient 로 선언해야 한다.
readResolve 사용 목적이 instance 개수를 통제하기 위함이라면 모두 transient 로 선언하자
만약 이러한 singleton 이 transient 가 아닌 참조 필드를 갖고 있다면 그 필드의 내용은 readResolve 가 실행되기 전에 역직렬화 된다. 그렇다면 앞선 MutualPeriod 와 같이 공격이 가능해지는데
스트림을 잘 조작하여 역직렬화 되는 시점에 그 역직렬화된 참조를 훔칠 수 있다.
더 상세한 설명을 위해 도둑(Stealer) 라는 클래스를 만든다. 이 클래스는 Elvis 의 직렬화된 singleton 을 참조하는 역할을 하고, 직렬화된 stream 에서 singleton 의 비휘발성 필드를 이 Stealer 의 instance로 교체한다.
public class Elvis implements Serializable {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {}
private String[] favoriteSongs = {"Hound Dog", "Heartbreak Hotel"}
public void printFavorite() {
System.out.println(Arrays.toString(favoriteSongs));
}
private Object readResolve() {
return INSTANCE;
}
}
Java
복사
잘못된 singleton transient 가 아닌 참조 필드를 갖고 있다.
public class ElvisStealer implements Serializable (
static Elvis impoersonator;
private Elvis payload;
private Object readResolve() {
impersonator = payload;
return new String[] {"A Fool Such as I"};
}
private static final long serialVersionUID = 0;
}
Java
복사
도둑 클래스
•
Stealer 는 Elvis 를 참조하고 있고, 역직렬화 될때 Stealer 의 readResolve 가 먼저 호출된다.
•
Stealer 가 역직렬화 될 때 Elvis 의 역직렬화가 실행되는데 역직렬화 도중인(그리고 readResolve 가 수행되기 전) 싱글턴의 참조가 담겨있게 된다.
•
Stealer 는 이
public class ElvisImpersonator {
// 진짜 Elvis 인스턴스로는 만들어질 수 없는 바이트 스트림
private static final byte[] serializedForm = {
-84, -19, 0, 5, 115, 114, 0, 42, 109, 101, 46, 121, 101, 118, 103, 110, 101, 110, 108, 108, 46, 99, 111, 110,
99, 117, 114, 114, 101, 110, 116, 46, 115, 101, 114, 105, 97, 108, 105, 122, 97, 98, 108, 101, 46, 69, 108, 118,
105, 115, -42, -124, -118, 12, 32, 123, 72, 7, 2, 0, 1, 91, 0, 13, 102, 97, 118, 111, 114, 105, 116, 101, 83,
111, 110, 103, 115, 116, 0, 19, 91, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110,
103, 59, 120, 112, 117, 114, 0, 19, 91, 76, 106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 83, 116, 114, 105, 110,
103, 59, -83, -46, 86, -25, -23, 29, 123, 71, 2, 0, 0, 120, 112, 0, 0, 0, 2, 116, 0, 9, 72, 111, 117, 110, 100,
32, 68, 111, 103, 116, 0, 16, 72, 101, 97, 114, 116, 98, 114, 101, 97, 107, 32, 72, 111, 116, 101, 108
};
public static void main(String[] args) {
// ElvisStealer.impersonator 를 초기화 한 다음
// 진짜 Elvis (즉, Elvis.INSTANCE)를 반환한다
Elvis elvis = (Elvis)deserialize(serializedForm);
Elvis impersonator = ElvisStealer.impersonator;
elvis.printFavorite();
impersonator.printFavorite();
}
static Object deserialize(byte[] sf) {
try {
return new ObjectInputStream(new ByteArrayInputStream(sf)).readObject();
} catch (IOException | ClassNotFoundException e) {
throw new IllegalArgumentException(e);
}
}
}
Java
복사
직렬화의 허점을 이용해 싱글턴 객체를 2개 생성한다.
실제 결과
책에서는 이러한 결과가 나온다고 한다.
favoriteSongs 를 transient 로 선언하면 문제를 고칠 수 있다고 한다.
이러한 문제는 직렬화가 가능한 클래스에 참조는 enum 을 선언하는 경우 딱 선언한 상수 이외에 다른 객체가 존재하지 않음을 java 가 보장해준다.
물론 공격자가 AccessibleObject.setAccessible 같은 privileged method 를 사용한다면 이야기가 달라진다. 다음은 Elvis 를 enum 을 사용한 예시이다.
public enum Elvis {
INSTANCE;
private String[] favoriteSongs = {
"Hound Dog", "Heartbreak Hotel"
};
public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs));
}
}
Java
복사
readResolve 를 사용하는 방식이 완전히 쓸모없는 것은 아니다. 직렬화 가능 instance 를 작성해야 하는데 compile 타임에 어떤 instance 가 있는지 알 수 없는 상황이라면 enum 으로 표현하는 것은 불가능하다
readResolve 의 접근성은 매우 중요하다
•
final class 라면 readResolve 는 반드시 private 이어야 한다
•
private 으로 선언하면 하위 클래스에서 사용할 수 없으므로 package-private 으로 선언하여 같은 패키지에 속한 하위 클래스에서만 사용할 수 있게 한다.
•
protected 나 public 으로 선언하면 이를 재정의하지 않은 모든 하위 클래스에서 사용할 수 있다.
•
하위 클래스의 instance 를 역직렬화 하면 상위 클래스의 instance 를 생성하여 ClassCastException 을 일으킬 수 있다.