Groovy 코드 주입

Groovy 코드 주입

설명

Groovy 코드 주입은 신뢰할 수 없는 문자열이 Groovy 소스 코드로 해석될 때 발생합니다. Eval, GroovyShell.evaluate/run, Script.evaluate는 전달된 표현식이나 스크립트를 실행합니다. GroovyShell.parse와 GroovyClassLoader.parseClass는 소스를 실행 가능한 스크립트 또는 클래스로 컴파일하며, 컴파일 단계에서도 Grape 의존성 해석이나 AST 변환 같은 기능이 개입할 수 있습니다.

공격자가 소스 텍스트를 제어하면 애플리케이션 권한으로 데이터와 파일을 읽거나 변경하고, 내부 네트워크에 접근하거나 프로세스를 실행하며, 무한 루프나 과도한 할당으로 자원을 고갈시킬 수 있습니다. 입력에서 특정 문자나 알려진 메서드 이름을 제거하는 방식은 Groovy 문법과 메타프로그래밍 경로를 완전하게 제한하지 못하므로 안전한 새니타이징 방법이 아닙니다.

잠재적 영향

  • 애플리케이션 권한으로 임의 코드 또는 운영체제 명령 실행
  • 파일, 환경 변수, 자격 증명, 데이터베이스 및 내부 서비스 접근
  • 애플리케이션 데이터 변경과 권한 경계 우회
  • CPU, 메모리, 스레드 또는 출력 고갈을 통한 서비스 거부

해결 방법

  1. Groovy 소스는 개발자가 관리하는 고정 문자열로 유지합니다. 동적 스크립트 대신 Java 로직을 사용하거나, 사용자가 선택할 수 있는 유한한 키를 검토된 고정 함수 또는 스크립트에 매핑합니다.
  2. 고정 스크립트가 요청 값을 사용해야 한다면 Binding 또는 Eval의 값 인자로 전달해 데이터로 취급합니다. 값을 소스 문자열에 연결하거나 포매팅하지 않습니다.
  3. 사용자 작성 코드를 반드시 지원해야 한다면 애플리케이션 프로세스 밖의 별도 프로세스, 컨테이너 또는 가상 머신에서 실행합니다. 애플리케이션 비밀, 불필요한 마운트와 네트워크 접근을 제거하고, 비특권 계정과 CPU·메모리·프로세스·출력·실행 시간 제한을 적용하며 제한 초과 시 실행을 종료합니다.
  4. SecureASTCustomizer는 허용할 문법을 줄이는 심층 방어 수단일 뿐 완전한 샌드박스가 아닙니다. 동적 스크립트가 남아 있다면 필요하지 않은 Grape와 컴파일 시점 기능을 비활성화하고 허용 기능을 최소화하되, 커스텀 ClassLoader 차단 목록이나 Java Security Manager를 보안 경계로 사용하지 않습니다. JDK 24부터 Security Manager는 영구적으로 비활성화되었습니다.
  5. 지원되는 최신 안정 Groovy 릴리스와 보안 패치를 적용합니다. 스크립트 식별자와 실행 결과를 감사하되 민감한 소스나 바인딩 값을 로그에 남기지 않습니다.

예시

변경 전

java
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class UnsafeGroovyController {
    @PostMapping("/exec")
    Object execute(@RequestParam("script") String userScript) {
        Script script = new GroovyShell().parse(userScript); // BAD: 사용자 입력을 컴파일
        return script.run();
    }
}

parse는 나중에 실행할 Script를 만들 뿐 안전하게 검증하지 않습니다. 또한 소스가 컴파일 단계의 기능을 사용할 수 있으므로 run() 호출 전이라고 안전한 경계가 되는 것도 아닙니다.

변경 후

가능하면 동적 언어 실행 경로를 제거하고 서버가 소유한 동작만 선택하게 합니다.

java
import java.util.Map;
import java.util.function.IntUnaryOperator;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class SafeController {
    private static final Map<String, IntUnaryOperator> OPERATIONS = Map.of(
            "double", value -> value * 2,
            "square", value -> value * value);

    @GetMapping("/calculate")
    int calculate(@RequestParam("operation") String operation,
                  @RequestParam("value") int value) {
        IntUnaryOperator selected = OPERATIONS.get(operation);
        if (selected == null) {
            throw new IllegalArgumentException("unsupported operation");
        }
        return selected.applyAsInt(value);
    }
}

Groovy가 필요해도 소스가 고정되어 있다면 외부 값은 바인딩 데이터로 분리할 수 있습니다.

java
import groovy.lang.Binding;
import groovy.lang.GroovyShell;

Object doubleValue(int value) {
    Binding binding = new Binding();
    binding.setVariable("value", value);
    return new GroovyShell(binding).evaluate("return value * 2");
}

참조