JNDI 주입 (JNDI Injection)

신뢰할 수 없는 JNDI 조회 이름으로 인한 리소스 주입

설명

JNDI(Java Naming and Directory Interface)는 이름을 사용해 객체와 컨텍스트를 조회합니다. 신뢰할 수 없는 값이 lookup, lookupLink 또는 InitialContext.doLookup의 이름이 되면 공격자가 애플리케이션이 해석할 리소스 식별자를 선택할 수 있습니다.

특히 InitialContext는 scheme_id:... 형태의 문자열을 URL 이름으로 취급하고 해당 스킴의 URL 컨텍스트 팩터리를 찾습니다. 따라서 공격자가 LDAP, RMI, DNS 또는 배포 환경에 추가된 커스텀 공급자를 선택하게 하여 외부·내부 네트워크 연결, 의도하지 않은 리소스 접근, 정보 노출 또는 서비스 지연을 유발할 수 있습니다. 반환된 참조를 객체로 재구성하는 동작은 JDK 버전, 공급자, 객체 팩터리, 클래스패스 및 직렬화 설정에 따라 코드 실행으로 이어질 수도 있습니다.

최신 JDK의 방어를 정확히 구분해야 합니다. JDK 24부터 기본 LDAP/RMI 공급자의 원격 팩터리 클래스 다운로드 경로는 영구적으로 비활성화되었고 기존 trustURLCodebase 속성은 제거되었습니다. JDK 25의 기본 LDAP 공급자는 com.sun.jndi.ldap.object.trustSerialData를 명시적으로 true로 설정하지 않는 한 LDAP 직렬화 데이터와 관련 참조를 재구성하지 않습니다. 그러나 URL 이름에 따른 네트워크 접근 자체와 로컬 객체 팩터리·커스텀 공급자·커스텀 ObjectFactoryBuilder의 위험은 별도이므로, 최신 JDK 사용만으로 공격자 제어 이름이 안전해지지는 않습니다.

잠재적 영향

  • 공격자가 선택한 LDAP, RMI, DNS 또는 커스텀 공급자에 대한 서버 측 연결
  • 내부 이름 서비스와 방화벽 뒤 리소스에 대한 접근 또는 정보 노출
  • 느리거나 응답하지 않는 공급자에 의한 스레드·연결 자원 고갈
  • 레거시 런타임, 직렬화 허용, 위험한 로컬 객체 팩터리 또는 커스텀 빌더가 있는 환경에서 객체 생성 부작용이나 코드 실행

해결 방법

  1. 전체 JNDI 이름을 개발자가 관리합니다. 외부에는 업무 키만 노출하고, 그 키를 완전한 switch, enum 또는 동등한 닫힌 매핑을 통해 고정된 로컬 이름으로 변환합니다. 신뢰할 수 없는 접미사를 JNDI 이름에 연결하지 않습니다.
  2. 호출자가 직접 이름을 제출해야 한다면 검토된 완전한 로컬 이름의 유한 집합만 정확히 비교하여 허용합니다. 접두사 검사, 정규식 또는 ldap:·rmi: 같은 스킴 차단 목록은 공급자, URL 컨텍스트 및 연합 이름 전체의 목적지를 증명하지 못합니다.
  3. Context.INITIAL_CONTEXT_FACTORY, Context.PROVIDER_URL, Context.OBJECT_FACTORIES, Context.URL_PKG_PREFIXES 및 공급자별 설정을 신뢰된 배포 구성으로 고정합니다. 신뢰된 공급자와 팩터리만 클래스패스에 두고 커스텀 ObjectFactoryBuilder를 별도로 검토합니다.
  4. 지원되는 최신 JDK 25 업데이트를 사용합니다. 제거된 trustURLCodebase 속성 설정을 해결책으로 제시하지 않습니다. com.sun.jndi.ldap.object.trustSerialData를 활성화하지 않고, 애플리케이션에 필요한 팩터리만 허용하도록 jdk.jndi.object.factoriesFilter와 공급자별 필터를 제한합니다.
  5. 필요한 이름 서버로만 송신 연결을 허용하고 최소 권한 자격증명을 사용합니다. 기본 LDAP 공급자를 사용한다면 com.sun.jndi.ldap.connect.timeout과 com.sun.jndi.ldap.read.timeout을 업무 요구에 맞게 설정합니다. 이 방어는 영향을 줄이지만 공격자 제어 이름을 안전하게 만들지는 않습니다.

예시

변경 전

java
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.http.HttpServletRequest;

class UnsafeJndiLookup {
    Object lookup(HttpServletRequest request) throws NamingException {
        String name = request.getParameter("name");
        Context context = new InitialContext();
        return context.lookup(name); // BAD: 요청 값이 공급자와 리소스를 선택할 수 있음
    }
}

변경 후

외부 키를 서버가 소유한 완전한 로컬 이름으로 변환합니다. switch 결과에는 요청 문자열이 포함되지 않습니다.

java
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.http.HttpServletRequest;

class SafeJndiLookup {
    Object lookup(HttpServletRequest request) throws NamingException {
        String key = request.getParameter("name");
        if (key == null) {
            throw new NamingException("resource not selected");
        }

        String jndiName = switch (key) {
            case "mainDs" -> "java:comp/env/jdbc/MainDS";
            case "ordersQ" -> "java:comp/env/jms/OrdersQueue";
            default -> throw new NamingException("resource not allowed");
        };

        return new InitialContext().lookup(jndiName);
    }
}

참조