JEXL 주입 (JEXL Injection)

JEXL 표현식, 스크립트 및 템플릿 주입

설명

Apache Commons JEXL은 문자열을 표현식이나 스크립트로 파싱하고 나중에 컨텍스트 데이터와 함께 실행합니다. JXLT는 JEXL을 사용하는 템플릿 엔진입니다. 신뢰할 수 없는 텍스트가 이들의 소스 인자에 도달하면 공격자가 애플리케이션이 실행할 연산과 제어 흐름을 작성할 수 있습니다.

영향은 JEXL 버전, 엔진 기능, 권한, 네임스페이스, 컨텍스트에 노출한 객체 및 실제 실행 여부에 따라 달라집니다. 공격자는 업무 조건을 변경하거나 노출된 객체의 데이터와 메서드에 접근하고 자원을 과도하게 사용할 수 있습니다. 광범위한 권한이나 레거시 설정에서는 파일, 네트워크, 리플렉션, 클래스 로딩 또는 프로세스 실행까지 가능할 수 있습니다.

JEXL 3.7.0은 기본 권한을 SECURE로 변경하고 new(...), 전역 부작용, pragma, annotation 등을 기본 기능에서 비활성화했습니다. 그러나 공식 문서는 SECURE와 RESTRICTED 모두 신뢰할 수 없는 사용자 입력을 실행하기 위한 완전한 샌드박스가 아니라고 명시합니다. 스크립트의 반복문은 기본 기능에서도 허용됩니다.

잠재적 영향

  • 평가 결과를 사용하는 인증, 가격, 필터 또는 업무 조건 변경
  • 컨텍스트, 네임스페이스 또는 허용된 클래스가 노출하는 데이터 조회와 상태 변경
  • 권한이 넓은 엔진에서 파일·네트워크·리플렉션·클래스 로딩·프로세스 API 접근
  • 긴 소스, 반복문, 큰 범위 또는 과도한 계산에 의한 CPU·메모리·출력 고갈

해결 방법

  1. 표현식, 스크립트 및 템플릿 소스는 개발자가 관리하는 고정 문자열로 유지합니다. 요청 값은 JexlContext 데이터나 선언된 스크립트 인자로만 전달하고 소스 문자열에 연결하지 않습니다.
  2. 사용자가 동작을 선택해야 한다면 유한한 서버 소유 키를 검토된 상수 프로그램이나 Java 함수에 매핑합니다. 단순 계산만 필요하면 더 작은 문법을 가진 전용 파서를 우선 사용합니다.
  3. 사용자 작성 JEXL이 제품 요구 사항이라면 지원되는 최신 릴리스를 사용하고 JexlPermissions.NONE 또는 JexlPermissions.create(...)에서 시작해 필요한 클래스와 멤버만 양의 규칙으로 허용합니다. UNRESTRICTED를 사용하거나 레거시 기본 설정을 복원하지 않으며, SECURE 또는 RESTRICTED만으로 충분하다고 가정하지 않습니다.
  4. JexlFeatures.createNone()에서 시작해 필요한 문법만 활성화합니다. 컨텍스트 객체, 네임스페이스, import, 산술 확장 및 클래스 로더 접근을 최소화합니다. JexlBuilder.uberspect(...)에 커스텀 구현을 전달하면 빌더 권한 동작을 대체하므로 별도로 검토해야 합니다.
  5. 소스 길이와 복잡도를 제한하고 실행 시간, CPU, 메모리 및 출력량을 강제 제한합니다. cancellable(true)를 구성하고 제한 시간에 실행 스레드를 중단하되, 강한 격리가 필요하면 애플리케이션 밖의 별도 프로세스나 컨테이너에서 실행합니다.
  6. 정규식이나 토큰 차단 목록은 임의의 JEXL 소스를 안전하게 만드는 새니타이저가 아닙니다. 정책은 허용할 문법과 객체 기능을 닫힌 집합으로 정의하고 실제 허용 결정을 테스트해야 합니다.

예시

변경 전

java
import org.apache.commons.jexl3.JexlBuilder;
import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.JexlScript;
import org.apache.commons.jexl3.MapContext;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class UnsafeJexlController {
    @PostMapping("/execute")
    Object execute(@RequestParam("script") String script) {
        JexlEngine engine = new JexlBuilder().create();
        JexlScript compiled = engine.createScript(script); // BAD: 요청을 코드로 파싱
        return compiled.execute(new MapContext());
    }
}

JEXL 3.7의 기본 설정은 이전 버전보다 엄격하지만, 요청 전체가 여전히 프로그램으로 해석됩니다. 특히 스크립트에는 반복문이 포함될 수 있고, 애플리케이션이 추가한 권한과 컨텍스트 객체가 공격 표면을 넓힙니다.

변경 후

소스를 고정하고 외부 값은 컨텍스트 데이터로 분리합니다.

java
import org.apache.commons.jexl3.JexlBuilder;
import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.JexlExpression;
import org.apache.commons.jexl3.MapContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class SafeJexlController {
    private final JexlEngine engine = new JexlBuilder().create();

    @GetMapping("/total")
    double total(@RequestParam("price") double price,
                 @RequestParam("quantity") int quantity) {
        MapContext context = new MapContext();
        context.set("price", price);
        context.set("quantity", quantity);

        JexlExpression fixed = engine.createExpression("price * quantity");
        return ((Number) fixed.evaluate(context)).doubleValue();
    }
}

사용자 작성 문법이 꼭 필요한 경우 다음과 같이 닫힌 권한·기능 집합에서 시작할 수 있지만, 이는 입력 제한과 자원 격리를 대체하지 않습니다.

java
import org.apache.commons.jexl3.JexlBuilder;
import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.JexlFeatures;
import org.apache.commons.jexl3.introspection.JexlPermissions;

JexlEngine constrained = new JexlBuilder()
        .permissions(JexlPermissions.create()) // 기본적으로 모든 리플렉션 접근 거부
        .features(JexlFeatures.createNone())   // 필요한 문법만 명시적으로 추가
        .cancellable(true)
        .strict(true)
        .silent(false)
        .create();

참조