Comments
link to Github.
package rule76; /** * readObject 메서드는 방어적으로 구현하라 * * 생성자에서 인자의 유효성을 검사한다면, readObject도 마찬가지로 인자의 유효성을 검사해야 한다. * 역직렬화할 때는 클라이언트가 private 필드의 참조를 클라이언트가 컨트롤할 수 있는 참조로 바꿀 수 없도록 방어적으로 구현해야 한다. * * @author gwon * @history * 2019. 6. 4. initial creation */ public class Rule76 { }
package rule76.compare1; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; /** * 유효성을 파괴하는 클래스 * * readObject 메서드가 실질적으로 public 생성자나 마찬가지며, 생성자를 구현할 때와 같은 점에 주의해야 한다. * 생성자와 마찬가지로 유효성 검사를 하지 않을 경우, 쉽게 클래스의 유효성을 망가뜨릴 수 있게 된다. * 기본 직렬화를 사용해서 readObject를 구현하지 않을 경우, 악의적인 사용자는 바이트스트림을 조작하여 데이터의 유효성을 파괴한다. * * * @author gwon * @history * 2019. 6. 4. initial creation */ public class ValidationConstructorClassNotGood implements Serializable { private final Integer notZero; // 유효성 체크를 하는 생성자 public ValidationConstructorClassNotGood(int num) { this.notZero = num; // 0일 경우 에러 if (this.notZero == 0) { throw new IllegalArgumentException(); } } @Override public String toString() { return "ValidationConstructorClassNotGood [notZero=" + notZero + "]"; } public static void main(String[] args) throws IOException, ClassNotFoundException { // 1로 객체 생성 ValidationConstructorClassNotGood original = new ValidationConstructorClassNotGood(1); System.out.println(original); // ValidationConstructorClassNotGood [notZero=1] // 직렬화 byte[] serializedBytes; try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { oos.writeObject(original); serializedBytes = baos.toByteArray(); } } /** * 이 클래스를 직렬화 할 경우, 가장 마지막 byte의 값이 nonZero 값이다. * nonZero을 0으로 변경 (유효성 파괴) */ serializedBytes[serializedBytes.length - 1] = 0x00; // 역직렬화 ValidationConstructorClassNotGood copy; try (ByteArrayInputStream bais = new ByteArrayInputStream(serializedBytes)) { try (ObjectInputStream ois = new ObjectInputStream(bais)) { Object objectMember = ois.readObject(); copy = (ValidationConstructorClassNotGood) objectMember; } } // nonZero 값이 0으로 변경됨을 확인 System.out.println(copy); // ValidationConstructorClassNotGood [notZero=0] } }
package rule76.compare1; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; /** * 역직렬화를 할 때도 유효성을 체크하는 클래스 * * readObject 메서드가 실질적으로 public 생성자나 마찬가지며, 생성자를 구현할 때와 같은 점에 주의해야 한다. * 생성자와 마찬가지로 유효성 검사를 하지 않을 경우, 쉽게 클래스의 유효성을 망가뜨릴 수 있게 된다. * readObject에서 public 생성자와 동일한 유효성을 체크하도록 해야 한다. * * @author gwon * @history * 2019. 6. 4. initial creation */ public class ValidationConstructorClassGoodCase implements Serializable { private final Integer notZero; // 유효성 체크를 하는 생성자 public ValidationConstructorClassGoodCase(int num) { this.notZero = num; // 0일 경우 에러 if (this.notZero == 0) { throw new IllegalArgumentException(); } } // compare : 유효성 체크를 하는 역직렬화 readObject private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException { s.defaultReadObject(); // 0일 경우 에러 if (notZero == 0) { throw new IllegalArgumentException(); } } @Override public String toString() { return "ValidationConstructorClassGoodCase [notZero=" + notZero + "]"; } public static void main(String[] args) throws IOException, ClassNotFoundException { // 1로 객체 생성 ValidationConstructorClassGoodCase original = new ValidationConstructorClassGoodCase(1); System.out.println(original); // ValidationConstructorClassGoodCase [notZero=1] // 직렬화 byte[] serializedBytes; try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { oos.writeObject(original); serializedBytes = baos.toByteArray(); } } /** * 이 클래스를 직렬화 할 경우, 가장 마지막 byte의 값이 nonZero 값이다. * nonZero을 0으로 변경 (유효성 파괴) */ serializedBytes[serializedBytes.length - 1] = 0x00; // 역직렬화 // compare : readObject의 유효성 체크에 걸려 IllegalArgumentException 발생 ValidationConstructorClassGoodCase copy; try (ByteArrayInputStream bais = new ByteArrayInputStream(serializedBytes)) { try (ObjectInputStream ois = new ObjectInputStream(bais)) { Object objectMember = ois.readObject(); copy = (ValidationConstructorClassGoodCase) objectMember; } } // compare : 도달하지 못함. System.out.println(copy); } }
package rule76.compare2; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; import java.util.Date; /** * * @author gwon * @history * 2019. 6. 4. initial creation */ public class PeriodNotGood implements Serializable { private final Date start; private final Date end; public PeriodNotGood(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(); } } // 유효성 체크를 하는 역직렬화 readObject private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException { s.defaultReadObject(); if (this.start.compareTo(this.end) > 0) { throw new IllegalArgumentException(); } } @Override public String toString() { return "Period [start=" + start + ", end=" + end + "]"; } }
package rule76.compare2; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.Date; /** * 특정 객체를 직렬화 하여 생긴 바이트스트림에 해당 객체 내부의 필드의 대한 참조 값을 추가로 붙여 쓸 수 있다. * 악의적인 사용자는 해당 참조값을 자신이 컨트롤할 수 있는 변수에 할당 한 후, 해당 객체에서 접근 불가능하도록 한(private) 필드 등을 조작할 수 있다. * * 이를 방어하기 위해, 해당 객체는 역직렬화할 때 클라이언트가 가질 수 없어야 하는 객체 참조를 담은 모든 필드를 방어적으로 복사애햐한다. * 즉, readObject 메서드 안에서 private로 선언된 필드들을 복사해서 참조를 해도 변경할 수 없도록 해야 한다. * * @author gwon * @history * 2019. 6. 4. initial creation */ public class MutableClass { // 접근 불가능한(private) 필드를 훔칠 클래스 (훔쳐짐) public final PeriodNotGood periodNotGood; public final Date startNotGood; public final Date endNotGood; // 접근 불가능한(private) 필드를 훔칠 클래스 (훔칠 수 없음) public final PeriodGood periodGood; public final Date startGood; public final Date endGood; public MutableClass() { try { /** * 훔칠 수 있는 periodNotGood */ ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bos); out.writeObject(new PeriodNotGood(new Date(), new Date())); // Period.start 필드의 참조를 뒤에 추가로 붙여 씀 byte[] ref = { 0x71, 0, 0x7e, 0, 5 }; bos.write(ref); // Period.end 필드의 참조를 뒤에 추가로 붙여 씀 ref[4] = 4; bos.write(ref); ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); periodNotGood = (PeriodNotGood) in.readObject(); startNotGood = (Date) in.readObject(); // start의 참조를 훔침 endNotGood = (Date) in.readObject(); // end의 참조를 훔침 /** * 훔칠 수 없는 periodGood */ ByteArrayOutputStream bos2 = new ByteArrayOutputStream(); ObjectOutputStream out2 = new ObjectOutputStream(bos2); out2.writeObject(new PeriodGood(new Date(), new Date())); // Period.start 필드의 참조를 뒤에 추가로 붙여 씀 byte[] ref2 = { 0x71, 0, 0x7e, 0, 5 }; bos2.write(ref2); // Period.end 필드의 참조를 뒤에 추가로 붙여 씀 ref2[4] = 4; bos2.write(ref2); ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(bos2.toByteArray())); periodGood = (PeriodGood) in2.readObject(); startGood = (Date) in2.readObject(); // start의 참조를 훔침 endGood = (Date) in2.readObject(); // end의 참조를 훔침 } catch (Exception e) { throw new AssertionError(e); } } public static void main(String[] args) { MutableClass.periodNotGoodTest(); MutableClass.periodGoodTest(); } /** * 훔칠 수 있는 PeriodNotGood 테스트 * * 값이 조작됨 */ public static void periodNotGoodTest() { System.out.println("########### PeriodNotGood Test ###########"); MutableClass mutableClass = new MutableClass(); Date endOfBrokenClass = mutableClass.endNotGood; // 참조를 훔친 변수 PeriodNotGood brokenClass = mutableClass.periodNotGood; System.out.println(brokenClass); // Period [start=Wed Jun 05 00:32:51 KST 2019, end=Wed Jun // 05 00:32:51 KST 2019] endOfBrokenClass.setYear(55); // 값 조작 System.out.println(brokenClass); // Period [start=Wed Jun 05 00:32:51 KST 2019, end=Sun Jun // 05 00:32:51 KDT 1955] } /** * 훔칠 수 없는 PeriodGood 테스트 * * 방어적으로 복사하여 값이 조작되지 않음 */ public static void periodGoodTest() { System.out.println("########### PeriodGood Test ###########"); MutableClass mutableClass = new MutableClass(); Date endOfBrokenClass = mutableClass.endGood; // 참조를 훔친 변수 PeriodGood brokenClass = mutableClass.periodGood; System.out.println(brokenClass); // Period [start=Wed Jun 05 00:32:51 KST 2019, end=Wed Jun // 05 00:32:51 KST 2019] endOfBrokenClass.setYear(55); // 값 조작 System.out.println(brokenClass); // Period [start=Wed Jun 05 00:32:51 KST 2019, end=Wed Jun // 05 00:32:51 KST 2019] } }
package rule76.compare2; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; import java.util.Date; /** * * @author gwon * @history * 2019. 6. 4. initial creation */ public class PeriodGood implements Serializable { // compare : 방어적 복사를 위해 final 제거 됨 private Date start; private Date end; public PeriodGood(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(); } } // 유효성 체크를 하는 역직렬화 readObject private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException { s.defaultReadObject(); // compare : private 필드를 모두 방어적으로 복사해서 해당 참조값이 변경되도 어떠한 영향이 없도록 함. start = new Date(start.getTime()); end = new Date(end.getTime()); if (this.start.compareTo(this.end) > 0) { throw new IllegalArgumentException(); } } @Override public String toString() { return "Period [start=" + start + ", end=" + end + "]"; } }