1. 외부 명령 실행(ProcessBuilder / 쉘 실행)을 악용한 커맨드 인젝션(Command Injection) 및 시스템 파괴 공격을 사전에 차단하기 위한 방어 코드
- Path Manipulation(경로 조작)
- OS Command Injection (명령 주입 공격)
import java.util.*;
import java.util.regex.Pattern;
/**
* 확장된 금지 명령어 검사기 (Java)
* - ProcessBuilder 인자 리스트 또는 쉘 커맨드를 사전에 검사해서 금지 토큰/명령을 발견하면 예외를 던집니다.
* - 금지 바이너리 목록은 실무에서 흔히 위험한 셸/인터프리터/관리 유틸을 포함하도록 확장되어 있습니다.
*
* 주의: 금지목록만으로는 항상 완전한 방어가 되지 않습니다. 가능하면 화이트리스트(허용 목록) 사용을 권장합니다.
*/
public final class ForbiddenCommandChecker {
// 확장된 금지된 실행파일(기본 이름만 검사). 필요시 절대경로도 처리하도록 확장 가능.
private static final Set FORBIDDEN_BINARAMES = Set.of(
// 쉘/인터프리터
"sh", "bash", "dash", "ksh", "zsh", "mksh", "ash", "fish",
// 일반적인 시스템/관리 유틸 (위험한 파일/디스크/유저 관련)
"rm", "rmdir", "mv", "cp", "dd", "mkfs", "fdisk", "parted", "mount", "umount",
"chmod", "chown", "chgrp", "ln", "lns", "useradd", "userdel", "usermod", "groupadd", "groupdel",
"passwd", "chpasswd", "pwck", "pwconv",
// 프로세스/서비스 제어
"kill", "killall", "pkill", "nice", "renice", "systemctl", "service", "init", "shutdown",
"reboot", "halt", "poweroff", "telinit", "runlevel",
// 네트워크/리모트 유틸
"ssh", "scp", "sftp", "telnet", "nc", "netcat", "ncat", "socat", "curl", "wget", "ftp", "tftp",
"ifconfig", "ip", "iptables", "nft", "route", "arp", "ss", "netstat",
// 스크립트 언어 실행기
"python", "python3", "perl", "ruby", "php", "node", "nodejs", "lua", "groovy",
// 빌드/패키지/관리
"apt", "apt-get", "yum", "dnf", "zypper", "pacman", "rpm", "dpkg", "pip", "pip3", "gem", "npm", "mvn", "gradle", "ant",
// 파일 전송/편집 도구
"vi", "vim", "nano", "ed", "sed", "awk", "rsync",
// 기타 유해 가능성 높은 유틸
"crontab", "at", "batch", "systemd-run", "screen", "tmux"
);
// 금지 토큰(정확 매칭)
private static final Set FORBIDDEN_TOKENS = Set.of(
";", "&&", "|", "`", "$(", ">", ">>", "<", "2>&1", ";&", ";&"
);
// 위험한 패턴(정규식) - 쉘 인젝션이나 치명적 옵션 등을 잡아냄
private static final List FORBIDDEN_PATTERNS = List.of(
// 명령줄에 ; 또는 && 또는 | 등이 들어가는 경우
Pattern.compile(".*(;|\\&\\&|\\|).*", Pattern.DOTALL),
// backtick 또는 $() 서브쉘
Pattern.compile(".*(`|\\$\\().*", Pattern.DOTALL),
// rm -rf 형태 (공백/탭 허용), 대소문자 무시
Pattern.compile("(?i).*\\brm\\b\\s+-\\s*[-rfv]+.*", Pattern.DOTALL),
// 민감 경로 접근 시도 (/etc, /root, /boot 등)
Pattern.compile(".*(/etc|/root|/boot|/proc|/sys|/dev).*", Pattern.DOTALL)
);
private ForbiddenCommandChecker() {}
public static class ForbiddenCommandException extends SecurityException {
public ForbiddenCommandException(String message) {
super(message);
}
}
/**
* ProcessBuilder 같은 인자 리스트를 검사한다.
* - 첫번째 인자(실행 파일) 이름이 금지 목록에 있는지 검사
* - 각 인자에 금지 토큰/패턴이 포함되어 있는지 검사
*/
public static void checkArgs(List args) {
if (args == null || args.isEmpty()) return;
String first = args.get(0);
String baseName = extractBaseName(first);
if (isForbiddenBinary(baseName)) {
throw new ForbiddenCommandException("금지된 실행파일 사용 시도: " + baseName);
}
for (String arg : args) {
if (arg == null) continue;
if (isForbiddenToken(arg)) {
throw new ForbiddenCommandException("금지된 토큰 발견(인자): " + arg);
}
if (matchesForbiddenPattern(arg)) {
throw new ForbiddenCommandException("금지된 패턴 발견(인자): " + arg);
}
// 추가 검증 포인트: 만약 인자가 경로라면 canonical 검사 등을 여기에 추가 가능
}
}
/**
* 쉘 전체 커맨드 문자열을 검사한다. (쉘을 쓰는 경우에만)
*/
public static void checkCommandLine(String cmdLine) {
if (cmdLine == null || cmdLine.isBlank()) return;
for (String token : FORBIDDEN_TOKENS) {
if (cmdLine.contains(token)) {
throw new ForbiddenCommandException("금지된 토큰이 명령행에 포함됨: " + token);
}
}
if (matchesForbiddenPattern(cmdLine)) {
throw new ForbiddenCommandException("금지된 패턴이 명령행에서 발견됨: " + cmdLine);
}
String[] parts = cmdLine.split("\\s+");
if (parts.length > 0) {
String bn = extractBaseName(parts[0]);
if (isForbiddenBinary(bn)) {
throw new ForbiddenCommandException("금지된 실행파일 사용 시도: " + bn);
}
}
}
// 헬퍼
private static boolean isForbiddenBinary(String baseName) {
if (baseName == null) return false;
return FORBIDDEN_BINARAMES.contains(baseName.toLowerCase());
}
private static boolean isForbiddenToken(String s) {
return FORBIDDEN_TOKENS.contains(s);
}
private static boolean matchesForbiddenPattern(String s) {
for (Pattern p : FORBIDDEN_PATTERNS) {
if (p.matcher(s).matches()) return true;
}
return false;
}
private static String extractBaseName(String pathOrName) {
if (pathOrName == null) return null;
int i = Math.max(pathOrName.lastIndexOf('/'), pathOrName.lastIndexOf('\\'));
return (i >= 0 && i + 1 < pathOrName.length()) ? pathOrName.substring(i + 1) : pathOrName;
}
// 간단한 테스트용 main
public static void main(String[] args) {
try {
checkArgs(Arrays.asList("ls", "-l", "/tmp"));
System.out.println("Args OK");
} catch (ForbiddenCommandException e) {
System.err.println("검사 실패: " + e.getMessage());
}
try {
checkArgs(Arrays.asList("/bin/bash", "-c", "echo hi"));
System.out.println("Should not print");
} catch (ForbiddenCommandException e) {
System.err.println("금지검출(예상): " + e.getMessage());
}
try {
checkCommandLine("ls -l; rm -rf /");
System.out.println("Should not print");
} catch (ForbiddenCommandException e) {
System.err.println("쉘 검사 실패(예상): " + e.getMessage());
}
}
}