의미만 들어보면 리플렉션은 보안적인 문제가 있을 가능성이 있어보이는데, 실제로 그렇게 생각하시나요? 만약 그렇다면, 어떻게 방지할 수 있을까요?
네, 실제로 보안 문제가 존재합니다. private 접근 제어를 런타임에 우회할 수 있다는 것 자체가 보안 취약점이 될 수 있습니다.
어떤 보안 문제가 있나?
1. 캡슐화 파괴
class BankAccount {
private int balance = 1000000; // private으로 보호
private String password = "secret123";
}
// 리플렉션으로 private 필드 강제 접근
Field balanceField = BankAccount.class.getDeclaredField("balance");
balanceField.setAccessible(true); // private 무력화
balanceField.set(account, 999999999); // 잔액 임의 변경 X
Field passwordField = BankAccount.class.getDeclaredField("password");
passwordField.setAccessible(true);
String password = (String) passwordField.get(account); // 비밀번호 탈취 X
2. 싱글톤 파괴
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() { return INSTANCE; }
}
// 리플렉션으로 private 생성자 강제 호출
Constructor<Singleton> constructor =
Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton instance2 = constructor.newInstance(); // 두 번째 인스턴스 생성 X
System.out.println(Singleton.getInstance() == instance2); // false
3. 직렬화 취약점
// 역직렬화 시 리플렉션으로 생성자 없이 객체 생성 가능
// → 악의적인 바이트스트림으로 임의 객체 생성 공격 가능
ObjectInputStream ois = new ObjectInputStream(maliciousStream);
Object obj = ois.readObject(); // 리플렉션 기반 역직렬화 X
방지 방법
1. 싱글톤 - 열거형(Enum) 사용
// O Enum은 리플렉션으로 생성 자체가 불가능
public enum Singleton {
INSTANCE;
public void doSomething() { ... }
}
// 시도해도 예외 발생
Constructor<Singleton> c = Singleton.class.getDeclaredConstructor();
c.newInstance(); // IllegalArgumentException: Cannot reflectively create enum objects
2. 생성자에서 방어 코드
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private static boolean created = false;
private Singleton() {
// 두 번째 생성 시도 차단
if (created) {
throw new RuntimeException("리플렉션으로 생성 불가"); // O
}
created = true;
}
}
3. SecurityManager 설정 (Java 17 이하)
// 리플렉션 접근 자체를 정책으로 제한
System.setSecurityManager(new SecurityManager() {
@Override
public void checkPackageAccess(String pkg) {
if (pkg.startsWith("com.example.secure")) {
throw new SecurityException("접근 불가 패키지"); // O
}
}
});
4. Java 9+ 모듈 시스템
// module-info.java
module com.example {
// exports만 선언된 패키지만 외부 접근 가능
exports com.example.api;
// opens 없으면 리플렉션 접근 불가
// opens com.example.internal; // 선언 안 하면 리플렉션 차단 O
}
// 모듈로 보호된 클래스에 리플렉션 시도
Field f = ProtectedClass.class.getDeclaredField("secret");
f.setAccessible(true); // InaccessibleObjectException 발생 O
5. 역직렬화 방어
// O ObjectInputFilter로 허용 클래스만 역직렬화
ObjectInputStream ois = new ObjectInputStream(inputStream);
ois.setObjectInputFilter(filterInfo -> {
Class<?> clazz = filterInfo.serialClass();
if (clazz != null && !allowedClasses.contains(clazz)) {
return ObjectInputFilter.Status.REJECTED; // 허용 목록 외 차단
}
return ObjectInputFilter.Status.ALLOWED;
});
정리
| 취약점 | 방지 방법 |
|---|---|
| 싱글톤 파괴 | Enum 사용 또는 생성자 방어 코드 |
| private 필드 접근 | Java 9+ 모듈 시스템 |
| 역직렬화 공격 | ObjectInputFilter 화이트리스트 |
| 패키지 접근 | SecurityManager (17 이하) |
결국 리플렉션의 보안 문제는 "강력한 기능에는 그만큼의 책임이 따른다" 는 것입니다. 프레임워크 내부에서는 필수적이지만, 일반 애플리케이션 코드에서
setAccessible(true)를 남발하는 것은 설계가 잘못됐다는 신호로 봐야 합니다.