', '<', '(', ')', '{', '}', '*', '?', '!', '\\', '"', "'", "\n", "\r", "\t", ]; /** * Shell 타입 command 를 실행해도 되는지 판정합니다. * * 게이트가 켜져 있고, 화이트리스트가 비어있지 않으며, 메타문자가 없고, * 첫 토큰의 basename 이 화이트리스트와 완전 일치할 때만 true. * * @param string $command 스케줄에 저장된 shell command 문자열 * @return bool 실행을 허용하면 true */ public static function isShellCommandAllowed(string $command): bool { if (! (bool) config('schedule_security.shell.enabled', false)) { return false; } $allowed = array_values(array_filter(array_map( static fn ($binary): string => trim((string) $binary), (array) config('schedule_security.shell.allowed_binaries', []), ))); if ($allowed === []) { return false; } $tokens = self::tokenize($command); if ($tokens === null || $tokens === []) { return false; } return in_array(basename($tokens[0]), $allowed, true); } /** * Shell command 를 `Process::run(array)` 에 넘길 안전한 인자 배열로 변환합니다. * * 메타문자가 섞여 있으면 셸 미경유 실행이 불가하므로 null 을 반환한다. * * @param string $command 스케줄에 저장된 shell command 문자열 * @return array|null 인자 배열, 안전하게 토큰화할 수 없으면 null */ public static function tokenizeShellCommand(string $command): ?array { return self::tokenize($command); } /** * Artisan command 문자열에서 명령명(첫 토큰)을 추출합니다. * * @param string $command 스케줄에 저장된 artisan command 문자열 * @return string|null 명령명, 빈 문자열이면 null */ public static function extractArtisanCommandName(string $command): ?string { $trimmed = trim($command); if ($trimmed === '') { return null; } $tokens = preg_split('/\s+/', $trimmed); return ($tokens === false || $tokens === []) ? null : $tokens[0]; } /** * Artisan command 가 차단목록에 걸리지 않는지 판정합니다. * * @param string $command 스케줄에 저장된 artisan command 문자열 * @return bool 실행을 허용하면 true */ public static function isArtisanCommandAllowed(string $command): bool { $name = self::extractArtisanCommandName($command); if ($name === null) { return false; } $denylist = array_map( static fn ($denied): string => strtolower(trim((string) $denied)), (array) config('schedule_security.artisan.denylist', []), ); return ! in_array(strtolower($name), $denylist, true); } /** * 메타문자 없는 공백 구분 토큰화. 메타문자가 있으면 null 을 반환합니다. * * 따옴표 escape 처리 버그를 원천 회피하기 위해, 따옴표를 포함한 복잡한 명령은 * 지원하지 않고 거부하는 보수적 구현이다(복합 명령은 래퍼 스크립트로 유도). * * @param string $command command 문자열 * @return array|null 토큰 배열, 안전하지 않으면 null */ private static function tokenize(string $command): ?array { $command = trim($command); if ($command === '') { return null; } // 제어문자(개행 포함)가 섞인 명령은 즉시 거부 if (preg_match('/[\x00-\x1F\x7F]/', $command) === 1) { return null; } foreach (self::SHELL_METACHARACTERS as $meta) { if (str_contains($command, $meta)) { return null; } } $tokens = preg_split('/\s+/', $command); return ($tokens === false || $tokens === []) ? null : $tokens; } }