728x90
자바 직렬화의 대안을 찾으라.
자바의 역직렬화는 명백하고 현존하는 위험이다. 이 기술은 지금도 애플리케이션에서 직접 혹은,
자바 하부시스템(RMI, JMX, JMS)을 통해 간접적으로 쓰이고 있기 때문이다.
신뢰할 수 없는 스트림을 역직렬화하면 원격 코드 실행(RCE), 서비스 거부(DoS)등의 공격으로 이어질 수 있다. 잘못한게 아무것도 없는 애플리케이션이라도 이런 공격에 취약해질 수 있다.
직렬화의 근본적인 문제는 공격 범위가 너무 넓고 지속적으로 더 넓어져 방어하기 어렵다는 점이다. ObjectInputStream의 readObject 메서드를 호출하면서 객체 그래프가 역직렬화되기 때문이다. readObject는 거의 모든 타입의 코드를 수행 할 수 있다.
가젯(써드파티 라이브러리)을 통해 역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 메서드들을 찾을 수 있지만 네이티브 코드를 마음대로 실행 할 수 있는 가젯체인도 발견된다.
오래 걸리는 짧은 스트림을 역직렬화하는 것만으로도 서비스 거부 공격에 쉽게 노출된다.
- 스트림의 전체 크기는 5744바이트지만 역지렬화는 끝나지 않는다.
- HashSet인스턴스를 역직렬화하려면 그 원소들의 해시코드를 계산해야 한다.
- 해시코드를 계산하는데 $2^{100}$번 넘게 호출해야한다.
- 무언가가 잘못되었는지 신호도 주지 않는다.
- 직렬화 위험을 회피하는 가장 좋은 방법은 아무것도 역직렬화하지 않는 것이다.
Set<Object> root = new HashSet<>();
Set<Object> s1 = root;
Set<Object> s2 = new HashSet<>();
for(int i = 0; i < 100; i++) {
Set<Object> t1 = new HashSet<>();
Set<Object> t2 = new HashSet<>();
t1.add("foo");
s1.add(t1); s1.add(t2);
s2.add(t1); s2.add(t2);
s1 = t1;
s2 = t2;
}
return serialize(root);
}
Cross-platform structured-data representation
- 객체와 바이트 시퀀스를 변환해주는 다른 메커니즘
- 자바 직렬화보다 훨씬 간단하고 임의 객체 그래프를 자동으로 직렬화/역직렬화하지 않는다.
- 대신 속성-값 쌍의 집합으로 구성된 간단하고 구조화된 데이터 객체를 제공한다.
- 예 : JSON
레거시 시스템 때문에 자바 직렬화를 완전히 배제할 수 없을 때는 역직렬화하지 않는것이다. RMI는 절대 수용해서는 안된다.
직렬화를 피할 수 없고 역직렬화한 데이터가 안전한지 완전히 확신 할 수 없다면 객체 역직렬화 필터링을 사용하자.
- 특정 클래스를 받아들이거나 거부할 수 있다.
- '기본 수용'모드에서는 블랙리스트에 기록된 잠재적 위험한 클래스들을 거부할 수 있다.
- '기본 거부'모드에서는 화이트스트레 기록된 클래스들만 수용한다.
- 블랙리스트 방식보다는 화이트리스트 방식을 추천한다.
Serializable을 구현할지는 신중히 결정하라.
Seriablizable을 구현하면 릴리즈한 뒤에는 수정하기 어렵다. 한번 배포하게 되면 영원히 지원해야 한다.
Serializable구현하면 릴리스한 뒤에는 수정하기 어렵다.
- 캡슐화가 깨진다.
- 직렬화 내부 구현을 바꿀 경우 원래의 직렬화 형태와 달라지게 된다.
- 잘 설계하더라도 클래스를 개선하는 데 제약이 될 수 있다.
버그와 보안 구멍이 생길 위험이 높아진다.
- 객체 생성하는 우회적인 방법이 생기므로 불변식 깨짐과 허가되지 않은 접근에 쉽게 노출된다.
해당 클래스의 신버전을 릴리스할 때 테스트할 것이 늘어난다는 점이다.
- 직렬화/역직렬화를 모두 성공해야 한다.
- 충실히 복제해내는지를 반드시 확인해야 한다.
- 신버전 인스턴스 직렬화한 후 구버전으로 역직렬화할 수 있는지 그 반대도 확인해야 한다.
Serializable 구현 여부는 가볍게 결정할 사안이 아니다.
- 비용이 많이 드므로 이득과 비용을 잘 저울질 해야 한다.
- 역사적으로 BigInteger와 Instant같은 '값'클래스와 컬렉션 클래스들은 Serializable을 구현하고 스레드 풀처럼 '동작'하는 객체를 표현하는 클래스들은 대부분 Serializable을 구현하지 않았다.
상속용으로 설계된 클래스와 인터페이스 대부분 Serializable을 구현하면 안된다.
- 클래스를 확장하거나 구현하는 이에게 너무 큰 부담을 지우게 된다.
- finalizer메서드를 자신이 재정의를 못하게 하거나 final로 선언하여 finalizer공격에 대비해야 한다.
- Serializable을 사용하지 않을 때는 하위 클래스도 사용하지 않는다는 것을 고려해야 한다.
- 내부 클래스는 직렬화를 구현하지 말아야 한다.
- 단, 정적 멤버 클래스는 Serializable을 구현해도 된다.
728x90
댓글