소프트웨어(SW) 보안약점 진단원/구현단계 보안약점 제거 기준
구현단계 보안약점 기준 - 신뢰할 수 없는 데이터의 역직렬화
브루노W
2025. 6. 4. 22:10
유형 | 코드오류 |
보안약점 | 신뢰할 수 없는 데이터의 역직렬화 |
개요 |
직렬화(Serialization)는 프로그램에서 특정 클래스의 현재 인스턴스 상태를 다른 서버로 전달하기 위해 클래스의 인스턴스 정보를 바이트 스트림으로 복사하는 작업으로, 메모리 상에서 실행되고 있는 객체의 상태를 그대로 복제하여 파일로 저장하거나 수신측에 전달하게 된다.
역직렬화(Deserialization)는 반대 연산으로 바이너리 파일이나 바이트 스트림으로부터 객체 구조로 복원하게 된다.
이때, 송신자가 네트워크를 이용하여 직렬화된 정보를 수신자에게 전달하는 과정에서 공격자가 전송 또는 저장된 스트림을 조작할 수 있는 경우에는 신뢰할 수 없는 역직렬화를 이용하여 무결성 침해, 원격 코드 실행, 서비스 거부 공격 등이 발생할 수 있는 보안약점이다.
|
보안대책 |
신뢰할 수 없는 데이터를 역직렬화하지 않도록 응용프로그램을 구성한다.
민감정보 또는 중요정보를 전송 시 암호화 통신을 적용하지 못하는 경우, 송신 측에서 서명을 추가하고 수신 측에서 서명을 확인하여 데이터의 무결성을 검증한다.
또는, 신뢰할 수 있는 데이터의 식별을 위해 역직렬화 대상의 데이터가 사전에 검증된 클래스만을 포함하는지 검증하거나, 제한된 실행 권한을 구성하여 역직렬화 코드를 실행한다.
|
진단방법 |
java.io.ObjectInputStream.readObject()와 같이 각 언어에서 제공하는 역직렬화 함수를 확인하고, 해당 역직렬화 함수에서 사용하는 데이터가 신뢰할 수 있는 값인지 확인한다.
만약 사용자 입력 값, 소켓으로 입력 받은 값 등 출처를 신뢰할 수 없는 데이터를 검증하는 절차가 없으면 취약하다고 판정한다.
|
연관된 설계단계 기준 | - |
코드예제
● 안전하지 않은 코드 예 (Java)
public static void main(String[] args) throws IOException, GeneralSecurityException, ClassNotFoundException {
....
// map을 역직렬화 한다.
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data"));
sealedMap = (SealedObject) in.readObject();
in.close();
// 객체를 추출한다.
cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
signedMap = (SignedObject) sealedMap.getObject(cipher);
map = (SerializableMap<String, Integer>) signedMap.getObject();
}
맵(map)을 직렬화하고 역직렬화 하는 코드이다. 데이터를 전송할 경우 공격자가 바이트 스트림을 조작하여 역직렬화 공격이 가능한 객체를 생성할 수 있다.
● 안전한 코드 예 (Java)
public static void main(String[] args) throws IOException, GeneralSecurityException, ClassNotFoundException {
....
// map을 역직렬화 한다.
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data"));
sealedMap = (SealedObject) in.readObject();
in.close();
// 객체를 추출한다.
cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
signedMap = (SignedObject) sealedMap.getObject(cipher);
// 서명값 검증 과정에서 불일치 시 예외를 리턴하고, 일치 시 map 값을 읽는다.
if (!signedMap.verify(kp.getPublic(), sig)) {
throw new GeneralSecurityException("Map failed verification");
}
map = (SerializableMap<String, Integer>) signedMap.getObject();
}
서명 값을 검증하여 메시지 위변조를 방지할 수 있는 코드이다.
● 안전하지 않은 코드 예 (Java)
class DeserializeExample {
public static Object deserialize(byte[] buffer) throws IOException, ClassNotFoundException {
Object ret = null;
try (ByteArrayInputStream bais = new ByteArrayInputStream(buffer)) {
try (ObjectInputStream ois = new ObjectInputStream(bais)) {
ret = ois.readObject();
}
}
return ret;
}
}
바이트 배열의 입력값을 검증 없이 readObject()로 역직렬화하여 악의적인 코드가 실행될 수 있다.
● 안전한 코드 예 (Java)
public class WhitelistedObjectInputStream extends ObjectInputStream {
public Set<String> whitelist;
// WhilelistedObjectInputStream을 생성할 때 화이트 리스트를 입력받는다.
public WhitelistedObjectInputStream(InputStream inputStream, Set<String> wl) throws IOException {
super(inputStream);
whitelist = wl;
}
@Override
protected Class<?> resolveClass(ObjectStreamClass cls) throws IOException, ClassNotFoundException {
// ObjectStreamClass의 클래스명이 화이트 리스트에 있는지 확인한다.
if (!whitelist.contains(cls.getName())) {
throw new InvalidClassException("Unexpected serialized class", cls.getName());
}
return super.resolveClass(cls);
}
}
@RequestMapping(value = "/upload", method = RequestMethod.POST)
public Student upload(@RequestParam("file") MultipartFile multipartFile) throws ClassNotFoundException, IOException {
Student student = null;
File targetFile = new File("/temp/" + multipartFile.getOriginalFilename());
// 역직렬화 대상 클래스 이름의 화이트 리스트 생성한다.
Set<String> whitelist = new HashSet<String>(Arrays.asList(new String[] {"Student"}));
try (InputStream fileStream = multipartFile.getInputStream()) {
try (WhitelistedObjectInputStream ois = new WhitelistedObjectInputStream(fileStream, whitelist)) {
// 화이트리스트에 없는 역직렬화 데이터의 경우 예외 발생시킨다.
student = (Student) ois.readObject();
}
}
return student;
}
진단방법

java.io.ObjectInputStream.readObject()와 같이 각 언어에서 제공하는 역직렬화 함수를 확인하고 (①), 해당 역직렬화 함수에서 사용하는 데이터가 신뢰할 수 있는 값인지 확인한다(②).
만약 사용자 입력 값, 소켓으로 입력받은 값 등 출처를 신뢰할 수 없는 데이터를 검증하는 절차가 없으면 취약하다고 판정한다.
● 일반적인 진단의 예
public class G02 extends HttpServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
ObjectInputStream ois = null;
ImageInformation retImgInfo = null;
try {
// HttpServletRequest의 InputStream은 사용자 입력값이다.
ois = new ObjectInputStream(req.getInputStream()); ·····················②
retImgInfo = (ImageInformation) ois.readObject(); ······················①
retImgInfo.setImgUrl("SERVERIMGURL");
} catch (Exception e) {
...
} finally
...
}
● 정탐코드
private static void byXmlString( String xml ) {
XMLDecoder xd = null;
try {
xd = new XMLDecoder(new ByteArrayInputStream(xml.getBytes()));
} catch (Exception e) {
e.printStackTrace();
}
Object s2 = xd.readObject();
xd.close();
}
XMLEncoder와 XMLDecoder는 JDK에 포함된 컴포넌트로, javaBeans Persistence 기능을 제공하기 위해 XML 직렬화를 사용한다. XMLDecoder를 사용하여 사용자가 제공한 데이터를 readObject()를 실행하면서 역직렬화하는 경우 공격자의 명령이 실행될 수 있으므로 취약하다고 판정한다.
● 정탐코드
conn,addr = self.receiver_socket.accept()
data = conn.recv(1024)
return Pickle.loads(data)
서버가 특정 포트를 열어 클라이언트가 데이터를 보낼 때까지 기다렸다가 데이터를 받으면 역직렬화 코드가 실행된다. 이 경우 신뢰할 수 없는 데이터 값에 대한 검증 과정이 없으므로 취약하다고 판정한다.
● 오탐코드
pickled_data = pickle.dumps(data)
digest = hmac.new('shared-key', pickled_data, hashlib.sha1).hexdigest()
header = '%s' % (digest)
conn.send(header + ' ' + pickled_data)
● 오탐코드
conn,addr = self.receiver_socket.accept()
data = conn.recv(1024)
recvd_digest, pickled_data = data.split(' ')
new_digest = hmac.new('shared-key', pickled_data, hashlib.sha1).hexdigest()
if recvd_digest != new_digest:
print 'Integrity check failed'
else:
unpickled_data = pickle.loads(pickled_data)
HMAC을 사용하여 데이터 무결성을 검증하므로 취약하지 않다고 판정한다.