티스토리 뷰
반응형
[아이템 50]에서 불변인 날짜 범위 클래스를 만드는 데 가변인 Data필드의 불변을 유지하기 위해 생성자와 접근자에서 Date 객체를 방어적으로 복사하느라 코드가 길어졌었다.
//방어적 복사를 사용하는 불변 클래스
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()); // 가변인 Date 클래스의 위험을 막기 위해 새로운 객체로 방어적 복사를 한다.
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException(start + " after " + end);
}
}
public Date start() { return new Date(start.getTime()); }
public Date end() { return new Date(end.getTime()); }
public String toString() { return start + " - " + end; }
// ... 나머지 코드는 생략
}
이 클래스를 직렬화해야한다면?
- Period 클래스는 물리적 표현 = 논리적 표현이어서 기본 직렬화 형태도 무방해보임 but 불변식 보장 못함
- readObject()가 또 다른 public 생성자이므로 똑같이 인수가 유효한지 검사해야하고 [아이템 49] 필요하다면 매개변수를 방어적 복사해야함 [아이템 50] → 안하면 공격자는 쉽게 불변 깨뜨릴 수 있음
readObject 메서드
- readObjectsms 쉽게말해 매개변수로 바이트 스트림을 받는 생성자라 할 수 있음
- 정상적으로 생성된 인스턴스를 직렬화해 만들어진 바이트 스트림이 아닌 불변식을 깨뜨릴 의도로 임의 생성한 바이트 스트림을 건네면 문제가 생김
public class BogusPeriod {
// 정상적이지 않은 바이트스트림
private static final byte[] serializedForm = {
(byte)0xac, (byte)0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x06,
0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x40, 0x7e, (byte)0xf8,
0x2b, 0x4f, 0x46, (byte)0xc0, (byte)0xf4, 0x02, 0x00, 0x02,
0x4c, 0x00, 0x03, 0x65, 0x6e, 0x64, 0x74, 0x00, 0x10, 0x4c,
0x6a, 0x61, 0x76, 0x61, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x2f,
0x44, 0x61, 0x74, 0x65, 0x3b, 0x4c, 0x00, 0x05, 0x73, 0x74,
0x61, 0x72, 0x74, 0x71, 0x00, 0x7e, 0x00, 0x01, 0x78, 0x70,
0x73, 0x72, 0x00, 0x0e, 0x6a, 0x61, 0x76, 0x61, 0x2e, 0x75,
0x74, 0x69, 0x6c, 0x2e, 0x44, 0x61, 0x74, 0x65, 0x68, 0x6a,
(byte)0x81, 0x01, 0x4b, 0x59, 0x74, 0x19, 0x03, 0x00, 0x00,
0x78, 0x70, 0x77, 0x08, 0x00, 0x00, 0x00, 0x66, (byte)0xdf,
0x6e, 0x1e, 0x00, 0x78, 0x73, 0x71, 0x00, 0x7e, 0x00, 0x03,
0x77, 0x08, 0x00, 0x00, 0x00, (byte)0xd5, 0x17, 0x69, 0x22,
0x00, 0x78
};
public static void main(String[] args) {
Period p = (Period) deserialize(serializedForm);
System.out.println(p);
}
static Object deserialize(byte[] sf) {
try {
return new ObjectInputStream(new ByteArrayInputStream(sf)).readObject();
} catch (IOException | ClassNotFoundException e) {
throw new IllegalArgumentException(e);
}
}
}
- 위 코드는 종료시각이 시작 시각보다 앞서는 Period 인스턴스를 만들 수 있는 문제 (불변식 깨짐)
- 해결 방법 : readObject 에서 defaultReadObject를 호출한 후에 역직렬화된 객체가 유효한지 검사해야함 → 유효성 검사에 실패하면 InvalidObjectException을 던지게 하여 잘못된 역직렬화를 막을 수 있음
//유효성 검사를 수행하는 readObject 메서드 - 아직 부족하다!!
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject(); // 기본 직렬화를 수행한다.
if (start.compareTo(end) > 0) { // 유효성 검사를 수행한다.
throw new InvalidObjectException(start + " 가 " + end + " 보다 늦다.");
}
}
- 위 문제가 끝이 아니다. 정상 Period 인스턴스에서 시작된 바이트 스트림 끝에 private Date 필드로의 참조를 추가하면 가변 Period 인스턴스를 만들어 낼 수 있다.
- 공격자는 이 악의적인 객체 참조를 통해 Period 객체의 내부정보를 얻을 수 있는 문제가 있다.
- 이 참조로 Date 인스턴스들을 수정 가능 → 불변 깨짐
public class MutablePeriod {
public final Period period; //Period 인스턴스
public final Date start; //시작 시각 필드 - 외부에서 접근할 수 없어야함
public final Date end; //종료 시각 필드 - 외부에서 접근할 수 없어야함
public MutablePeriod() {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
// 불변식을 유지하는 Period 를 직렬화한다.
out.writeObject(new Period(new Date(), new Date()));
/*
* bos 값에 악의 적인 바이트스트림을 주입한다.
*/
byte[] ref = { 0x71, 0, 0x7e, 0, 5 }; // 악의적인 바이트스트림
bos.write(ref); // start 필드
ref[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); // end 필드를 80년으로 수정한다.
System.out.println(p);
pEnd.setYear(69); // end 필드를 60년으로 수정한다.
System.out.println(p);
}
위와 같은 공격으로 인해 엄청난 보안 문제가 생긴다.
→ 객체를 역직렬화할 때는 클라이언트가 소유해서는 안되는 객체 참조를 갖는 필드를 모두 반드시 방어적으로 복사해야한다!!
//방어적 복사와 유효성 검사를 수행하는 readObject 메서드
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 + "보다 늦다.");
}
}
final 필드는 방어적 복사가 불가능하니 readObject 메서드를 사용하려면 start와 end에 final을 제거해야함
기본 readObject 메서드를 써도 좋은지 판단하는 방법
- 판단 기준 : transient 필드를 제외한 모든 필드의 값을 매개변수로 받아 유효성 검사 없이 필드에 대입하는 public 생성자를 추가해도 괜찮은가?
- 아니오라면, 커스텀 readObject 메서드를 만들어 모든 유효성 검사와 방어적 복사 수행해야함 or 직렬화 프록시 패턴[아이템 90] 사용 (적극 권장)
- final이 아닌 직렬화 가능 클래스라면 readObject 메서드도 생성자처럼 재정의 가능 메서드를 호출해서는 안된다. [아이템 19] 이 규칙을 어겼는데 그 메서드가 재정의되면 하위 클래스의 상태가 완전히 역직렬화되기 전에 하위 클래스에서 재정의된 메서드가 실행된다. → 프로그램 오작동
결론
- readObject()를 작성할때는 언제나 public 생성자를 작성하는 것처럼 신중해야함. 어떤 바이트 스트림이 넘어오더라도 유효한 인스턴스를 만들어내야함
- private이어야하는 객체 참조 필드는 각 필드를 가리키는 객체를 방어적 복사하라(불변 클래스 내의 가변요소)
- 모든 불변식을 검사하여 어긋나는게 발견되면 InvalidObjectException을 던지자. 방어적 복사 다음엔 꼭 불변식 검사를 해야함
- 역직렬화 후 객체 그래프 전체의 유효성을 검사해야한다면 ObjectInputValidation 인터페이스 사용
- 직접적/간접적이든 재정의 가능 메서드는 호출하지 말자
반응형
'Java > Effective Java' 카테고리의 다른 글
[Effective Java] 87.커스텀 직렬화 형태는 고려해보라 (0) | 2022.05.10 |
---|---|
[Effective Java] 86.Serializable을 구현할지는 신중히 결정하라 (0) | 2022.05.10 |
[Effective Java] 85.자바 직렬화의 대안을 찾으라 (0) | 2022.05.10 |
[Effective Java] 84.프로그램의 동작을 스레드 스케줄러에 기대지말라 (0) | 2022.05.08 |
[Effective Java] 83.지연 초기화는 신중히 사용하라 (0) | 2022.05.08 |
댓글