SpEL, Janino 및 Java JSR-223 코드 삽입

신뢰할 수 없는 SpEL, Janino 및 Java JSR-223 실행 코드 입력

설명

Java와 Kotlin에서 신뢰할 수 없는 텍스트를 SpEL 표현식, Janino 스크립트 또는 JSR-223 스크립트 소스로 사용하면 코드 삽입 위험이 생깁니다.

SpEL은 생성자와 메서드를 호출하고 속성, 필드, Spring 빈을 참조할 수 있습니다. Janino는 전달받은 Java 문을 실제 바이트코드로 컴파일합니다. JSR-223은 선택된 스크립트 엔진의 언어로 소스를 실행하거나 재사용 가능한 형태로 컴파일합니다. ScriptEngine.eval은 즉시 실행하지만 SpEL 파싱, Janino 컴파일 및 Compilable.compile은 이후 평가가 있어야 실행됩니다. 엔진과 노출된 컨텍스트 또는 바인딩에 따라 공격자가 제어하는 코드가 애플리케이션과 같은 권한으로 실행될 수 있습니다.

Spring Framework 7.0.9의 보안 지침은 신뢰할 수 없는 SpEL 표현식의 평가가 본질적으로 위험하므로 일반적으로 피하라고 명시합니다. StandardEvaluationContext에는 전체 SpEL 기능이 노출되며, SimpleEvaluationContext의 제한도 최선 노력 방식일 뿐 안전을 보장하지 않습니다.

Java SE 25의 ScriptEngine API는 첫 번째 문자열 인수를 실행할 스크립트 소스로 정의하고, Bindings와 ScriptContext를 스크립트에 보이는 값과 객체를 전달하는 경로로 구분합니다. 따라서 신뢰할 수 없는 값은 고정 스크립트의 데이터로 전달해야 하며, 엔진 이름을 제한하거나 신뢰할 수 있는 구현을 선택하는 것만으로 신뢰할 수 없는 스크립트 소스가 안전해지지는 않습니다.

잠재적 영향

  • 애플리케이션 권한으로 임의 코드 또는 위험한 메서드 실행
  • 파일, 데이터베이스, 자격 증명 등 기밀 데이터의 유출 또는 변조
  • 프로세스 실행, 네트워크 접근, 권한 상승의 발판 제공
  • 과도한 컴파일 또는 평가로 인한 CPU·메모리 고갈과 서비스 거부

해결 방법

  1. SpEL 표현식, Janino 스크립트 및 JSR-223 스크립트 소스는 개발자가 관리하는 고정 문자열로 유지하고, 신뢰할 수 없는 텍스트를 파싱·컴파일·평가하지 마십시오.
  2. SpEL에는 고정된 표현식을 사용하고 요청 값은 변수, 루트 객체의 단순 데이터 또는 함수 인수로만 전달하십시오. 신뢰할 수 없는 표현식을 StandardEvaluationContext로 평가해서는 안 되며, SimpleEvaluationContext를 새니타이저나 안전 보장으로 취급해서도 안 됩니다.
  3. Janino에서는 고정된 스크립트를 컴파일하고 setParameters로 입력을 선언한 뒤, 요청 값은 소스 코드에 연결하지 말고 evaluate의 인수로 전달하십시오.
  4. JSR-223에서는 고정된 스크립트를 평가하고 요청 값은 Bindings 또는 ScriptContext로 전달하십시오. 스크립트에 꼭 필요한 값과 객체만 노출하고, 엔진 선택을 스크립트 소스 검증으로 취급하지 마십시오.
  5. 사용자가 작업을 선택해야 한다면 유한한 서버 관리 식별자를 검토된 고정 표현식이나 스크립트에 매핑하십시오. 길이 검사, 문자 이스케이프 또는 sanitize 같은 도우미 이름만으로 실행 입력을 안전하다고 판단하지 마십시오.
  6. 사용자가 실행 코드를 직접 작성해야 하는 기능은 애플리케이션 외부의 별도 프로세스나 컨테이너로 격리하고 최소 권한, 실행 시간, CPU, 메모리, 파일 시스템 및 네트워크 제한을 적용하십시오. JDK 24부터 Security Manager를 활성화할 수 없으므로 Java Security Manager나 Janino의 레거시 샌드박스 안내에 의존해서는 안 됩니다.

예시

SpEL

변경 전

java
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class DocumentController {
    private final SpelExpressionParser parser = new SpelExpressionParser();

    @GetMapping("/document")
    String evaluate(@RequestParam String expressionText) {
        Expression expression = parser.parseExpression(expressionText);
        return expression.getValue(new StandardEvaluationContext(), String.class);
    }
}

요청 값이 표현식 문법으로 파싱된 뒤 전체 SpEL 기능을 제공하는 컨텍스트에서 평가됩니다.

변경 후

java
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class DocumentController {
    private static final Expression TEXT_EXPRESSION =
        new SpelExpressionParser().parseExpression("#text");

    @GetMapping("/document")
    String render(@RequestParam String text) {
        StandardEvaluationContext context = new StandardEvaluationContext();
        context.setVariable("text", text);
        return TEXT_EXPRESSION.getValue(context, String.class);
    }
}

표현식은 개발자가 고정하고 요청 값은 표현식 소스가 아닌 데이터 변수로만 전달합니다.

Janino

변경 전

java
import org.codehaus.janino.ScriptEvaluator;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class ScriptController {
    @PostMapping("/script")
    Object run(@RequestParam String script) throws Exception {
        ScriptEvaluator evaluator = new ScriptEvaluator();
        evaluator.cook(script);
        return evaluator.evaluate();
    }
}

요청 값이 Java 스크립트 본문으로 컴파일되고 즉시 실행됩니다.

변경 후

java
import org.codehaus.janino.ScriptEvaluator;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class ScriptController {
    @PostMapping("/normalize")
    String normalize(@RequestParam String text) throws Exception {
        ScriptEvaluator evaluator = new ScriptEvaluator();
        evaluator.setReturnType(String.class);
        evaluator.setParameters(
            new String[] { "input" },
            new Class<?>[] { String.class }
        );
        evaluator.cook("return input.trim();");
        return (String) evaluator.evaluate(new Object[] { text });
    }
}

컴파일되는 스크립트는 고정되어 있고 요청 값은 선언된 매개 변수의 런타임 인수로만 전달됩니다.

JSR-223

변경 전

java
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class AutomationController {
    private final ScriptEngine engine;

    AutomationController(ScriptEngine engine) {
        this.engine = engine;
    }

    @PostMapping("/automation")
    Object run(@RequestParam String script) throws ScriptException {
        return engine.eval(script);
    }
}

요청 값이 JSR-223 스크립트 소스로 즉시 실행됩니다. 실제 언어와 접근 가능한 기능은 주입된 엔진 및 바인딩에 따라 달라집니다.

변경 후

java
import javax.script.Bindings;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class AutomationController {
    private static final String FIXED_SCRIPT = "input";
    private final ScriptEngine engine;

    AutomationController(ScriptEngine engine) {
        this.engine = engine;
    }

    @PostMapping("/automation")
    Object run(@RequestParam String input) throws ScriptException {
        Bindings bindings = engine.createBindings();
        bindings.put("input", input);
        return engine.eval(FIXED_SCRIPT, bindings);
    }
}

스크립트 소스는 선택한 엔진 언어에 맞춘 서버 관리 상수이고, 요청 값은 Bindings의 데이터로만 전달됩니다.

적용 시 주의사항

파싱·컴파일만 수행한 경우와 실제 평가·실행한 경우를 구분하고, 생성된 표현식이나 코드의 후속 사용을 확인하세요. 입력 전달 방식이나 도우미 함수의 이름이 달라져도 신뢰할 수 없는 텍스트를 실행 소스로 다루는 위험은 같습니다.

참조