Search
Duplicate
🧵

역직렬화(deserialize)의 주의할점 readObject 메서드는 방어적으로 작성하는 패턴

태그
Effective Java
Java
Serializable
공개여부
작성일자
2021/07/25
하나의 클래스를 직접 직렬화 하면서 무엇을 주의해야 하는지, Serializable 이 보안적인 측면에서 무엇을 주의해야 할지 다뤄본다. Serializable 을 지양하는 이유중에 하나가 보안인데 이 item 은 특히 이 부분을 대응하기 위해 어떻게 방어적인 패턴을 적용할지 살핀다.

88. Write readObject methods defensively

item 50 에서 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) { if (start.compareTo(end) > 0) throw new IllegalArgumentException(start + "가 " + end + "보다 늦다."); this.start = start; this.end = end; } public Date start() { return new Date(start.getTime()); } public Date end() { return new Date(end.getTime()); } }
Java
논리적 구현과 물리적 구현이 일치하여 기본 implements Serializable 만 붙여도 되겠다고 생각할 수 있지만 그렇지 않다.
이유는 이 클래스의 핵심적인 불변식을 보장하지 못하기 때문이다.

readObject 는 public API 이다.

접근 제어자가 private 이지만 역직렬화 과정에서 이것은 생성자와 동일하다.
따라서 생성자에서 수행하는 validation check 가 동일하게 수행되어야 한다.
public class BogusPeriod { // 진짜 Period 인스터느에서는 만들어질 수 없는 바이트 스트림 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); } // Returns the object with the specified serialized form static Object deserialize(byte[] sf) { try { return new ObjectInputStream(new ByteArrayInputStream(sf)).readObject(); } catch (IOException | ClassNotFoundException e) { throw new IllegalArgumentException(e); } } }
Java
위 역직렬화가 내 컴퓨터에선 진행이 되지 않는데 아마도 package 이름이 일치하지 않아서 그런것 같다.
하지만 결과는 end < start 로 불변식이 깨지게 된다.
즉, 클래스가 지켜야하는 규칙을 무시하고 Period 가 생성된다.
이 문제를 고치기 위해 다음과 같은 코드가 추가 되어야 한다.
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); if (start.compareTo(end) > 0) { throw new InvalidObjectException(start + "가 " + end + "보다 늦다."); } }
Java

악의적인 공격, 비잔틴 결함

start, end 는 final 변수이다. 하지만 이 참조에 접근하여 Date 인스턴스들을 수정할 수 있다.
public class MutablePeriod { public final Period period; public final Date start; public final Date end; public MutablePeriod() { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bos); out.writeObject(new Period(new Date(), new Date())); byte[] ref = { 0x71, 0, 0x7e, 0, 5}; bos.write(ref); ref[4] = 4; bos.write(ref); ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); period = (Period) in.readObject(); start = (Date) in.readObject(); end = (Date) in.readObject(); } catch (IOException | ClassNotFoundException | SecurityException 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); pEnd.setYear(69); System.out.println(p); } }
Java
이처럼 변경할 수 있는 Period 인스턴스를 획득하면 공격적는 이 인스턴스가 불변이라고 가정하는 클래스에 값을 수정하여 엄청난 보안 문제를 일으킬 수 있다.
이것이 위험한 이유는 String 도 불변이라는 사실에 기댄 class 가 존재하기 때문이다.
객체를 역직렬화 할 때는 client 가 소유해서는 안되는 객체 참조를 갖는 필드를 모두 반드시 방어적으로 복사해야 한다.
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 + "보다 늦다."); } }
Java
방어적 복사와 유효성 검사를 수행한다.
이때 start, end 는 final 을 제거해야 하는데 그럼에도 불구하고 같은 공격에 값이 변경되지 않는다.
final 필드들은 방어적 복사가 불가능하니 주의해야 한다.
그래서 readObject 를 사용하면 final 을 제거해야 하는데 아쉽지만 공격 위험에 노출되는 것 보다 낫다.

결론

transient 를 제외하고 모든 필드의 값을 매개변수로 받아 유효성 검사 없이 객체를 생성해도 괜찮은가?
만약 아니라면 custom readObject 를 만들어 유효성 검사와 방어적 복사를 수행해야 한다.
이 방어적 복사가 들어간 코드는 override 가 되지 않도록 해야한다.
만약 상속받은 곳에서 재정의가 가능하다면 위의 규칙이 깨진채로 역직렬화 되어 객체가 생성될 것이다.
readObject 는 public 생성자를 작성하는 수준으로 설계해야 한다.
어떤 byte stream 이 넘어오더라도 진짜 역직렬화 인스턴스라고 가정하지 말아야 한다.
private 이어야 하는 객체 참조 필드는 각 필드가 가리키는 객체를 방어적으로 복사하라 불변 클래스 내의 가변 요소가 여기 속한다.
모든 불변식을 검사하여 어긋나면 InvalidObjectException 을 던지고, 방어적 복사 다음에 불변식 검사를 수행해야 한다.
역직렬화 이후 그래프 전체의 유효성이 필요하면 ObjectInputValidation 을 사용해야 한다(이 책에서는 다루지 않는다)
직접적이든 간접적이든, override 할 수 있는 method 는 호출하지 않는다

직렬화 시리즈

직렬화(Serializ), 역직렬화에 대한 개념 정리

Effective Java 86 Serializable 을 구현할지 신중히 결정하자

Effective Java 87 커스텀 직렬화 형태를 고려해보자

Effective Java 88 readObject 는 방어적으로 작성하라

TOP