item 88) readObject 메서드는 방어적으로 작성하라
readObject 메서드는 실질적으로 또다른 public 생성자이므로 다른 생성자만큼 주의깊게 다뤄야 한다.
인수가 유효한지 검사하고, 매개변수를 반드시 방어적 복사하여 (item 50) 잘못된 역직렬화를 막을 수 있다.
가변 공격
정상 객체 인스턴스에서 시작된 바이트 스트림의 끝에 private 필드로의 참조를 추가하면 가변 객체 인스턴스를 만들어낼 수 있다.
아래는 직렬화된 바이트스트림에 악의적인 객체 참조를 추가해 내부 값에 접근 가능한 예제이다.
public class MutablePeriod {
//Period 인스턴스
public final Period period;
public final Date start;
public final Date end;
public MutablePeriod() {
try {
ByteArrayOutputStream bos =new ByteArrayOutputStream();
ObjectOutputStream out =new ObjectOutputStream(bos);
// 1) Period 인스턴스 직렬화
out.writeObject(new Period(new Date(),new Date()));
// 2) start, end 필드에 대한 참조 추가
byte[] ref = {0x71, 0, 0x7e, 0, 5};
bos.write(ref);
ref[4] = 4;
bos.write(ref);
// 3) Period 역직렬화 후 Date 참조를 훔친다.
ObjectInputStream in =new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
period = (Period)in.readObject();
start = (Date)in.readObject(); //Period 내부의 start 데이터 탈취
end = (Date)in.readObject(); //Period 내부의 end 데이터 탈취
} catch (IOException | ClassNotFoundException e) {
throw new AssertionError(e);
}
}
불변식과 불변 성질을 지키기 위해 모든 private 가변 요소를 방어적으로 복사하면 해결할 수 있다.
final 필드의 경우 final 한정자를 제거해야 방어적 복사가 가능하다.
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 + " after " + end); } }
기본 readObject 메서드
transient 필드를 제외한 모든 필드의 값을 매개변수로 받아 유효성 검사 없이 필드에 대입하는 public 생성자를 추가해도 괜찮다면 사용 가능
final이 아닌 직렬화 가능한 클래스라면 readObject 메서드도 재정의 가능한 메서드를 호출해서는 안 된다. (item 19)
하위 클래스의 상태가 역직렬화되기 전에 하위 클래스에서 재정의된 메서드가 실행될 수 있고, 이는 프로그램의 오작동으로 이어질 수 있다.
기본 readObject 메서드를 사용할 수 없다면, 직렬화 프록시 패턴을 사용하거나 커스텀 readObject 메서드를 만들어 모든 유효성 검사와 방어적 복사를 수행해야 한다.
Last updated