// 방어적 복사를 사용하는 불변 클래스
public final class Period {
private final Date start;
private final Date end;
/**
* @param start 시작 시각
* @param end 종료 시각; 시작 시각보다 뒤여야 한다.
* @throws IllegalArgumentException 시작 시각이 종료 시각보다 늦을 때 발생한다.
* @throws NullPointerException start나 end가 null이면 발행한다.
*/
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if(this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException(start + "가 " + end + "보다 늦다.");
}
}
public Date start() { return new Date(start.getTime()); }
public Date end() { return new Date(end.getTime()); }
public String toString() { return start + "-" + end; }
}
쉽게 말해 readObject는 매개변수로 바이트 스트림을 받는 생성자라 할 수 있다.
// 가변 공격의 예
public class MutablePeriod {
//Period 인스턴스
public final Period period;
//시작 시각 필드 - 외부에서 접근할 수 없어야 한다.
public final Date start;
//종료 시각 필드 - 외부에서 접근할 수 없어야 한다.
public final Date end;
public MutablePeriod() {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectArrayOutputStream out = new ObjectArrayOutputStream(bos);
//유효한 Period 인스턴스를 직렬화한다.
out.writeObject(new Period(new Date(), new Date()));
/**
* 악의적인 '이전 객체 참조', 즉 내부 Date 필드로의 참조를 추가한다.
* 상세 내용은 자바 객체 직렬화 명세의 6.4절을 참고하자.
*/
byte[] ref = {0x71, 0, 0x7e, 0, 5}; // 참조 #5
bos.write(ref); // 시작 start 필드 참조 추가
ref[4] = 4; //참조 #4
bos.write(ref); // 종료(end) 필드 참조 추가
// Period 역직렬화 후 Date 참조를 훔친다.
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
period = (Period) in.readObject();
start = (Date) in.readObject();
end = (Date) in.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new AssertionError(e);
}
}
}
// 공격이 실제로 이뤄지는 모습
public static void main(String[] args) {
MutablePeriod mp = new MutablePeriod();
Period p = mp.period;
Date pEnd = mp.end;
//시간을 되돌리자!
pEnd.setYear(78);
System.out.println(p);
//60년대로 회귀!
pEnd.setYear(60);
System.out.println(p);
}
위 공격 예시로 Period 인스턴스는 불변식을 유지한 채 생성됐지만, 의도적으로 내부의 값을 수정할 수 있다
이처럼 변경할 수 있는 Period 인스턴스를 획득한 공격자는 이 인스턴스가 불변이라고 가정하는 클래스에 넘겨 엄청난 보안 문제를 일으킬 수 있다.
객체를 역직렬화 할 때는 클라이언트가 소유해서는 안되는 객체 참조를 갖는 필드를 모두 반드시 방어적으로 복사해야 한다.
따라서 readObject에서는 불변 클래스 안의 모든 Private 가변 요소를 방어적으로 복사해야 한다.
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
// 가변 요소들을 방어적으로 복사한다.
start = new Date(start.getTime());
end = new Date(end.getTime());
// 불변식을 만족하는지 검사한다.
if(start.compareTo(end) > 0) {
throw new InvalidObjectException(start + "가 " + end + "보다 늦다.");
}
}