-
[이펙티브 자바] item 88 - readObject 메서드는 방어적으로 작성하라개발서적읽기/Effective Java - temp 2020. 10. 3. 00:05
■깨지기 쉬운 직렬화에서의 불변식
item 50에서는 불변인 날짜 범위 클래스를 만드는 데 가변 Date 필드를 이용했다. 그래서
불변식을 지키고 불변을 유지하기 위해 생성자와 접근자에서 Date 객체를 방어적으로
복사하느라 코드가 상당히 길어졌다. 아래가 바로 그 클래스다.
public final class Period {
private final Date start;
private final Date end;
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(this.start + "가 " + this.end + "보다 늦다.");
}
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
}Period 객체의 물리적 표현이 논리적 표현과 부합하므로, 이 클래스를 직렬화하기 위해선
Serializable 인터페이스를 구현하기만 하면 될 것 같다. 하지만 그러면 이 클래스의 중요한
불변식을 더는 보장하지 못하게 된다.
원인은 바로 readObject 메서드에 있다. readObject 메서드는 실질적으로 또 다른 public
생성자라고 할 수 있다. 따라서 생성자가 수행하는 조건들을 readObject에도 똑같이
수행해야 한다. (item 50) 그렇지 않으면 공격자는 아주 손쉽게 해당 클래스의 불변식을
깨뜨릴 수 있다.
■직렬화에서의 불변식 보완
readObject는 매개변수로 바이트 스트림을 받는 생성자라 할 수 있다. 바이트 스트림은
보통 정상적으로 생성된 인스턴스를 직렬화해서 만들어진다. 그런데 이 바이트 스트림을
의도적으로 수정하거나 생성하여 readObect에 건네면 문제가 생기게 된다. 정상적인
생성자로는 만들 수 없는 객체가 생성되기 때문이다.
이 문제를 고치려면 readObject 메서드가 defaultReadObject를 호출한 다음 역직렬화된
객체가 유효한지 검사해야 한다. 이 유효성 검사에 실패하면 InvalidObjectException을
던지게 하여 잘못된 역직렬화가 일어나는 것을 막을 수 있다.
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
// 불변식을 만족하는지 검사한다.
if(start.compareTo(end) > 0) {
throw new InvalidObjectException(start + "가 " + end + "보다 늦다.");
}
}하지만 아직 문제가 있다. 정상적으로 직렬화된 Period 인스턴스의 바이트 스트림 끝에
private Date 필드로의 참조를 추가하면 가변 Period 인스턴스를 만들어 낼 수가 있다.
공격자는 ObjectInputStream에서 Period 인스턴스를 읽은 후, 스트림 끝에 추가되어 있는
'악의적인 객체 참조'를 읽어 Period 객체의 내부 정보를 얻을 수 있다. 그리고 이 참조로
얻은 Date 인스턴스들을 검사 없이 수정해버릴 수도 있으니, Period 인스턴스의 필드는
더 이상 검사되지 않는다. 다음은 이 공격이 어떻게 이뤄지는지 보여주는 예다.
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); // Wed Nov 22 00:21:29 PST 2017 - Wed Nov 22 00:21:29 PST 1978
//60년대로 회귀
pEnd.setYear(60);
System.out.println(p); // Wed Nov 22 00:21:29 PST 2017 - Wed Nov 22 00:21:29 PST 1969
}이 예에서 Period 인스턴스는 불변식을 유지한 채 생성됐지만, 이렇게 의도적으로 내부의
값을 수정할 수 있다. 이처럼 변경할 수 있는 Period 인스턴스를 획득한 공격자는 이
인스턴스가 불변이라고 가정하는 클래스에 넘겨 엄청난 보안 문제를 일으킬 수 있다.
이러한 문제의 근원은 Period의 readObject()가 방어적 복사를 충분히 하지 않은 데 있다.
객체를 역직렬화할 때는 클라이언트가 소유해서는 안 되는, 객체 참조를 갖는 필드를 모두
반드시 방어적으로 복사해야 한다. 따라서 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 + "보다 늦다.");
}
}방어적 복사를 유효성 검사보다 앞서 수행했다. 만약 유효성 검사가 방어적 복사보다 앞에
있다면, 유효성 검사를 통과한 후 방어적으로 복사하기 전에 공격자가 참조를 통해서
Date값을 바꿔버리고 그 후에 방어적으로 복사하게 되기 때문인 듯 하다.
한편 final 필드는 방어적 복사가 불가능하기 때문에, start와 end 필드에서 final 한정자를
제거해야 한다.
■기본 readObject를 사용해도 되는 경우
transient 필드를 제외한 모든 필드의 값을 매개변수로 받아, 유효성 검사 없이 필드에
대입하는 public 생성자를 추가해도 괜찮은가? 그렇다면 기본 readObject를 사용해도 된다.
하지만 그렇지 않다면 커스텀 readObject()를 만들어 생성자에서의 유효성 검사와 동일한
수준의 검사를 해야 한다. 그리고 방어적 복사는 필수이다.
혹은 직렬화 프록시 패턴(item 90)을 사용하는 방법도 있다. (이 패턴은 역직렬화를 안전하게
만드는 데 필요한 노력을 상당히 경감해주므로 적극 권장된다)
'개발서적읽기 > Effective Java - temp' 카테고리의 다른 글
[이펙티브 자바] item 74 - 메서드가 던지는 모든 예외를 문서화하라 (0) 2020.09.18 [이펙티브 자바] item 66 - 네이티브 메서드는 신중히 사용하라 (0) 2020.09.11 [이펙티브 자바] item 59 - 라이브러리를 익히고 사용하라 (0) 2020.09.03 [이펙티브 자바] item 52 - 다중정의는 신중히 사용하라 (0) 2020.08.28 [이펙티브 자바] item 45 - 스트림은 주의해서 사용하라 (0) 2020.08.21 댓글