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 + "]";
	}

}