티스토리 뷰
728x90
반응형
readObject 메서드는 방어적으로 작성하라
- readObject 메서드는 실질적으로 또 다른 public 생성자로 볼 수 있습니다. 따라서 readObject 메서드에도 충분한 주의를 기울여야 합니다. 그렇지 않으면 불변식을 깨트릴 수 있습니다. 그렇기 때문에 readObject 메서드 또한 하나의 생성자라 생각하고 방어적으로 작성해야 합니다.
- readObject 메서드는 매개변수로 바이트 스트림을 받는 생성자라 할 수 있는데 보통의 경우 바이트 스트림은 정상적으로 생성된 인스턴스를 직렬화해 만들어집니다. 하지만 불변식을 깨트릴 목적으로 생성한 바이트 스트림을 건낸 경우 문제가 발생합니다.
💡 불변식을 깨트릴 수 잇는 코드
- 아래 코드는 Serializable 인터페이스를 선언하고 생성자에서 방어적 복사와 유효성 검사를 하고 있습니다. 이렇게 하고 직렬화를 한다면 아무런 문제가 없을걸로 보이지만 사실 readObject 메서드에서 방어적으로 작성하지 않았기 때문에 문제가 발생할 수 있습니다.
위에서 readObject 메서드는 또 다른 생성자라고 하였습니다. 그렇기 때문에 readObject 메서드를 선언하여 똑같이 작업을 해주어야 합니다.
public final class Period implements Serializable {
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());
}
}
💡 readObject 메서드 선언
- 위 문제를 해결하기 위해서는 readObject 메서드를 선언한 후 defaultReadObject 메서드를 호출한 다음 역직렬화된 객체가 유효한지 검사해야 합니다. 유효성에 실패하면 예외를 던지게 됩니다. 이렇게 하면 잘못된 역직렬화를 막을순 있지만 아직까지 문제가 있습니다. 정상적으로 직렬화된 Period 인스턴스의 바이트 스트림 끝에 private Date 필드로의 참조를 추가하게 되면 가변 Period 인스턴스를 만들어 낼 수 있습니다. 공격자는 ObjectInputStream에서 Period 인스턴스를 읽은 후 스트림 끝에 있는 악의적인 객체 참조를 읽어 Period 인스턴스 내부의 정보를 얻을 수 있습니다. 그리고 이 정보를 이용해 Date 인스턴스들을 검사 없이 수정해버릴 수 있습니다.
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(this.start + "가 " + this.end + "보다 늦다.");
}
💡 악의적인 코드
- 아래 예제에서는 Period 객체를 불변식을 가지고 생성되었지만 의도적으로 값을 수정할 수 있습니다.
- 문제가 발생하는 이유는 readObject 메서드에서 충분한 방어적 복사가 이루어지지 않아서 문제가 발생합니다. 이 또한 생성자이므로 방어적 복사가 이루어져야 합니다.
@ToString
public final class Period implements Serializable {
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());
}
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(this.start + "가 " + this.end + "보다 늦다.");
}
}
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);
//유효한 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 class Main {
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); // Period(start=Sat Aug 27 17:06:52 KST 2022, end=Sun Aug 27 17:06:52 KST 1978)
pEnd.setYear(60);
System.out.println(p); // Period(start=Sat Aug 27 17:06:52 KST 2022, end=Sat Aug 27 17:06:52 KDT 1960)
}
}
💡 방어 방법
- 기존 final start, end 필드를 readObject 메서드에서 방어적 복사를 하기위해서는 final 키워드를 제거해야 합니다.
- 또한 방어적 복사를 유효성 검사보다 앞서 수행해야 합니다. 만약 유효성 검사가 방어적 복사보다 앞에서 수행하게 된다면 유효성 검사를 통과한 후 방어적으로 복사하기 전에 공격자가 참조를 통해서 Date의 값을 수정하고 그 후 방어적 복사가 이루어지기 때문입니다.
public final class Period implements Serializable {
private Date start;
private 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());
}
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
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 + "보다 늦다.");
}
}
💡 기본 readObject 메서드를 사용해도 되는 경우
- 항상 readObject 메서드를 커스텀할 필요는 없습니다. 기존 readObject 메서드를 사용해도 되는지 판단하는 방법은 아래와 같습니다.
- transient 필드를 제외하고 모든 필드의 값을 매개변수로 받아 유효성 검사 없이 필드에 대입하는 public 생성자를 만들어도
괜찮은 경우 readObject 메서드를 재정의할 필요 없습니다.
- transient 필드를 제외하고 모든 필드의 값을 매개변수로 받아 유효성 검사 없이 필드에 대입하는 public 생성자를 만들어도
728x90
반응형
'스터디 > 이펙티브 자바' 카테고리의 다른 글
이펙티브 자바 - Item90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라 (0) | 2022.08.28 |
---|---|
이펙티브 자바 - Item86. Serialization을 구현할지는 신중히 결정하라 (0) | 2022.08.27 |
이펙티브 자바 - Item85. 자바 직렬화의 대안을 찾으라 (1) | 2022.08.27 |
이펙티브 자바 - Item83. 지연 초기화는 신중히 사용하라 (0) | 2022.08.25 |
이펙티브 자바 - Item82. 스레드 안전성 수준을 문서화하라 (0) | 2022.08.24 |
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
TAG
- 자바 백엔드 개발자 추천 도서
- 공간 기반 아키텍처
- JDK Dynamic Proxy와 CGLIB의 차이
- microkernel architecture
- space based architecture
- pipeline architecture
- spring boot 엑셀 다운로드
- @ControllerAdvice
- java ThreadLocal
- transactional outbox pattern
- spring boot redisson 분산락 구현
- 트랜잭셔널 아웃박스 패턴 스프링 부트 예제
- spring boot redisson sorted set
- polling publisher spring boot
- spring boot excel download oom
- spring boot redis 대기열 구현
- spring boot redisson destributed lock
- java userThread와 DaemonThread
- service based architecture
- 람다 표현식
- 트랜잭셔널 아웃박스 패턴 스프링부트
- redis sorted set으로 대기열 구현
- redis sorted set
- pipe and filter architecture
- transactional outbox pattern spring boot
- spring boot poi excel download
- redis 대기열 구현
- 서비스 기반 아키텍처
- 레이어드 아키텍처란
- spring boot excel download paging
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
글 보관함