운영 체제 명령어 및 인수 삽입

Java 프로세스의 실행 파일, 명령 문자열, 옵션 또는 인수 구성에 외부 입력이 사용될 때 발생하는 운영 체제 명령어 및 인수 삽입

설명

외부 입력으로 운영 체제 프로세스의 실행 파일이나 명령 문자열을 선택하면 공격자가 다른 프로그램을 실행할 수 있습니다. 실행 파일이 고정되어 있어도 외부 입력이 옵션이나 인수에 들어가면 대상 프로그램의 동작을 바꾸는 인수 삽입(argument injection) 이 발생할 수 있습니다. 예를 들어 find의 -exec처럼 다른 프로그램을 실행하는 옵션이 삽입되면 인수 삽입이 운영 체제 명령 실행으로 이어질 수 있습니다.

Java의 ProcessBuilder는 실행 파일과 인수를 문자열 목록의 개별 요소로 전달합니다. 셸을 명시적으로 호출하지 않는 경우 이 구조는 셸 메타문자 해석을 피하고 인수 경계를 유지하도록 돕지만, 각 옵션과 인수의 의미를 검증하지는 않습니다. 따라서 실행 파일을 고정하고 인수의 의미도 검증해야 합니다.

Runtime.exec(String)은 Java 18부터 사용 중단(deprecated) 상태이며, 문자열을 공백 단위로 나누고 셸을 자동 호출하지 않습니다. 이 오버로드 대신 실행 파일과 인수를 구분해서 전달하세요. 아래 예시는 셸 실행 파일, -c, 외부 명령 문자열을 별도 요소로 전달합니다.

Apache Commons Exec 1.6.0의 CommandLine.addArguments(String)도 따옴표와 공백을 파싱하며, 공식 API 문서는 이 파싱에 원치 않는 부작용이 있을 수 있으므로 명령줄을 점진적으로 구성하라고 권고합니다. 다만 개별 addArgument 호출 역시 인수의 보안상 의미를 검증하지는 않습니다.

잠재적 영향

  • 애플리케이션 권한으로 임의의 명령이나 프로그램 실행
  • 파일, 자격 증명, 환경 변수 등 민감한 데이터 열람 또는 변경
  • 네트워크 접근, 권한 상승의 발판 마련, 서비스 중단

피해 범위는 자식 프로세스가 상속하는 계정 권한, 파일 시스템 및 네트워크 접근 권한에 따라 달라집니다.

해결 방법

  1. 가능한 경우 외부 프로세스를 시작하지 말고 같은 작업을 수행하는 Java 라이브러리나 API를 사용합니다.
  2. 프로세스가 꼭 필요하면 실행 파일 경로를 고정하고, 사용자가 선택할 수 있는 작업을 서버가 관리하는 유한한 인수 템플릿에 매핑합니다.
  3. 실행 파일과 각 인수를 ProcessBuilder의 별도 요소로 전달합니다. 가변 옵션과 피연산자는 대상 명령과 위치에 맞는 엄격한 허용 목록으로 검증합니다. 함수 이름이 sanitize, escape인 것만으로 안전하다고 간주하지 않습니다.
  4. --는 대상 유틸리티가 옵션 종료 구분자로 지원한다고 문서화하고 외부 입력이 피연산자 위치에 있을 때만 사용합니다. 모든 프로그램에 통하는 일반 살균 방법으로 취급하지 않습니다.
  5. 신뢰할 수 없는 값을 sh -c, cmd /c, PowerShell -Command, python -c와 같은 셸·인터프리터의 코드 위치에 전달하지 않습니다.
  6. 자식 프로세스는 필요한 최소 권한으로 실행합니다.

예시

변경 전

다음 코드는 외부 입력을 POSIX 셸의 명령 문자열 위치에 직접 전달하므로 실제 셸 구문으로 평가됩니다.

java
import java.io.IOException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class CommandController {
    @GetMapping("/run")
    void run(@RequestParam String command) throws IOException {
        new ProcessBuilder("/bin/sh", "-c", command).start();
    }
}

셸을 사용하지 않아도 외부 입력으로 대상 프로그램의 옵션을 선택하게 하면 동작이 바뀔 수 있습니다.

변경 후

다음 코드는 외부 입력을 명령줄에 복사하지 않고, 허용된 작업 식별자를 서버가 소유한 고정 인수 목록에 매핑합니다.

java
import java.io.IOException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
class OperationController {
    @GetMapping("/run")
    void run(@RequestParam String operation) throws IOException {
        String[] argv = switch (operation) {
            case "date" -> new String[] {"/usr/bin/date", "--iso-8601=seconds"};
            case "identity" -> new String[] {"/usr/bin/id", "-u"};
            default -> throw new IllegalArgumentException("Unsupported operation");
        };

        new ProcessBuilder(argv).start();
    }
}

외부 입력을 파일명, 호스트명 같은 데이터 인수로 전달해야 한다면 별도 요소로 전달하는 것에 더해 해당 명령과 인수 위치에 맞는 형식, 길이, 허용 값 범위를 검증해야 합니다.

참조