설명
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·메모리 고갈과 서비스 거부
해결 방법
- SpEL 표현식, Janino 스크립트 및 JSR-223 스크립트 소스는 개발자가 관리하는 고정 문자열로 유지하고, 신뢰할 수 없는 텍스트를 파싱·컴파일·평가하지 마십시오.
- SpEL에는 고정된 표현식을 사용하고 요청 값은 변수, 루트 객체의 단순 데이터 또는 함수 인수로만 전달하십시오. 신뢰할 수 없는 표현식을
StandardEvaluationContext로 평가해서는 안 되며,SimpleEvaluationContext를 새니타이저나 안전 보장으로 취급해서도 안 됩니다. - Janino에서는 고정된 스크립트를 컴파일하고
setParameters로 입력을 선언한 뒤, 요청 값은 소스 코드에 연결하지 말고evaluate의 인수로 전달하십시오. - JSR-223에서는 고정된 스크립트를 평가하고 요청 값은
Bindings또는ScriptContext로 전달하십시오. 스크립트에 꼭 필요한 값과 객체만 노출하고, 엔진 선택을 스크립트 소스 검증으로 취급하지 마십시오. - 사용자가 작업을 선택해야 한다면 유한한 서버 관리 식별자를 검토된 고정 표현식이나 스크립트에 매핑하십시오. 길이 검사, 문자 이스케이프 또는
sanitize같은 도우미 이름만으로 실행 입력을 안전하다고 판단하지 마십시오. - 사용자가 실행 코드를 직접 작성해야 하는 기능은 애플리케이션 외부의 별도 프로세스나 컨테이너로 격리하고 최소 권한, 실행 시간, CPU, 메모리, 파일 시스템 및 네트워크 제한을 적용하십시오. JDK 24부터 Security Manager를 활성화할 수 없으므로 Java Security Manager나 Janino의 레거시 샌드박스 안내에 의존해서는 안 됩니다.
예시
SpEL
변경 전
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 기능을 제공하는 컨텍스트에서 평가됩니다.
변경 후
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
변경 전
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 스크립트 본문으로 컴파일되고 즉시 실행됩니다.
변경 후
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
변경 전
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 스크립트 소스로 즉시 실행됩니다. 실제 언어와 접근 가능한 기능은 주입된 엔진 및 바인딩에 따라 달라집니다.
변경 후
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의 데이터로만 전달됩니다.
적용 시 주의사항
파싱·컴파일만 수행한 경우와 실제 평가·실행한 경우를 구분하고, 생성된 표현식이나 코드의 후속 사용을 확인하세요. 입력 전달 방식이나 도우미 함수의 이름이 달라져도 신뢰할 수 없는 텍스트를 실행 소스로 다루는 위험은 같습니다.
참조
- Spring Framework 7.0.9 — SpEL Security Considerations
- Spring Framework — ExpressionParser API
- Janino 3.1.12 — ScriptEvaluator source and API
- Janino 3.1.12 — ICookable source and API
- Java SE 25 — ScriptEngine API
- Java SE 25 — Compilable API
- Java SE 25 — Bindings API
- OpenJDK JEP 486 — Permanently Disable the Security Manager
- OWASP Top 10:2025 A05 — Injection
- OWASP Top 10:2021 A03 — Injection
- CWE-94 — Improper Control of Generation of Code