Files
Gnuboard7/tests/Unit/Installer/InstallerSecurityHardeningTest.php
T
HeuJung 9cf9c3ff8c fix(core,board,ecommerce,payments,basic): KVE-2026 보안 게이트 6묶음 + 자격증명 전송로 정합
KISA 제보 취약점(KVE-2026-1914/1919/2019/2029/2041/2042/2043/2044)과
그 수정 과정에서 드러난 자격증명 전송로 결함을 함께 해소한다.

globalHeaders 는 데이터소스와 apiCall 핸들러에만 적용되는데, 코어 ApiClient 를
직접 부르는 경로들이 그 사실을 모른 채 게이트된 엔드포인트를 호출하고 있었다.
서버는 정당한 사용자를 거부하고 화면은 이미 버튼을 내준 뒤라, 예외도 로그도 없이
그 자리만 비는 형태로만 드러났다. 전송로 10축을 전수 열거해 6건을 고치고,
같은 실수가 반복되지 않도록 규정과 coverage 에 등재했다.

아웃바운드 프록시가 사이트 자기 자신으로 가는 내부 요청까지 가로채 저장이 수십 초씩
걸리던 문제도 함께 고쳤다. 실패가 폴백으로 삼켜져 화면에는 지연으로만 나타났다.
2026-09-06 00:42:29 +09:00

664 lines
27 KiB
PHP

<?php
namespace Tests\Unit\Installer;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use ValidationApi;
/**
* 인스톨러 추가 보안 강화 회귀 테스트
*
* 후속 감사로 추가 발견된 5개 이슈에 대한 회귀 가드:
* - getComposerCommand/Display 의 공백 분기 RCE
* - save-extensions 식별자 셸 주입
* - checkCorePendingPath 경로 traversal/정보 노출
* - escapeEnvValue 개행 라인 주입
* - PDO DSN 파라미터 주입
*/
class InstallerSecurityHardeningTest extends TestCase
{
private static bool $loaded = false;
private static string $tempBase = '';
private static string $skipReason = '';
public static function setUpBeforeClass(): void
{
if (self::$loaded) {
return;
}
require_once __DIR__.'/stubs/lang_stub.php';
if (! defined('MIN_PHP_VERSION')) {
define('MIN_PHP_VERSION', '8.2.0');
}
if (! defined('CHECK_CONFIGURATION_LIBRARY')) {
define('CHECK_CONFIGURATION_LIBRARY', true);
}
// BASE_PATH 미정의 시 프로젝트 루트로 정의 — 같은 PHPUnit 프로세스에서 뒤에
// 실행되는 인접 테스트(DeleteDirectoryTest, InstallerWindowsCommandsTest 등)가
// `require_once BASE_PATH . '/public/install/...'` 로 로드하는 패턴이 깨지지 않도록.
$projectRoot = dirname(__DIR__, 3);
if (! defined('BASE_PATH')) {
define('BASE_PATH', $projectRoot);
}
// STATE_PATH 는 테스트 격리용 임시 경로로 분리 — 운영 storage/installer-state.json 미오염.
self::$tempBase = sys_get_temp_dir().'/g7-installer-hardening-'.bin2hex(random_bytes(4));
@mkdir(self::$tempBase.'/storage/app', 0755, true);
if (! defined('STATE_PATH')) {
define('STATE_PATH', self::$tempBase.'/storage/installer-state.json');
}
// 안전 가드: STATE_PATH 는 PHP 상수라 한 프로세스에서 단 한 번만 정의된다.
// 다른 Installer 테스트(ComposerPathValidationTest 등)가 먼저 BASE_PATH 기반
// 루트 경로로 STATE_PATH 를 박으면 본 테스트의 writeState/clearState 가 실제 운영
// installer-state.json 을 파괴하게 된다. STATE_PATH 가 시스템 temp 하위가 아니면 skip.
$tempPrefix = realpath(sys_get_temp_dir()) ?: sys_get_temp_dir();
$resolvedState = realpath(dirname((string) STATE_PATH)) ?: dirname((string) STATE_PATH);
if (strpos($resolvedState, $tempPrefix) !== 0) {
self::$skipReason = 'STATE_PATH ('.STATE_PATH.') 가 시스템 temp 하위가 아님 — '
.'다른 Installer 테스트의 상수 정의가 선행됨. 실제 installer-state.json 파괴 방지를 위해 skip. '
.'격리 실행: php vendor/bin/phpunit --filter=InstallerSecurityHardeningTest';
self::$loaded = true;
return;
}
require_once $projectRoot.'/public/install/api/check-configuration.php';
require_once $projectRoot.'/public/install/includes/installer-state.php';
require_once $projectRoot.'/public/install/includes/functions.php';
require_once $projectRoot.'/public/install/includes/task-runner.php';
self::$loaded = true;
}
protected function setUp(): void
{
parent::setUp();
if (self::$skipReason !== '') {
$this->markTestSkipped(self::$skipReason);
}
}
private function invokePrivate(object $obj, string $method, array $args)
{
$ref = new ReflectionClass($obj);
$m = $ref->getMethod($method);
return $m->invokeArgs($obj, $args);
}
/**
* 테스트가 도중 종료되어도 임시 디렉토리에 state.json 잔여물이 남지 않도록 정리.
* STATE_PATH 는 setUpBeforeClass 의 안전 가드로 시스템 temp 하위임이 보장된 경우에만
* 이 지점에 도달하므로, 운영 storage/installer-state.json 은 절대 건드리지 않는다.
* (과거 루트 state 를 무조건 @unlink 하던 코드는 실제 운영 설치 상태를 파괴할 수 있어 제거)
*/
protected function tearDown(): void
{
if (self::$skipReason === '' && is_file(STATE_PATH)) {
@unlink(STATE_PATH);
}
parent::tearDown();
}
/**
* 테스트별 임시 state.json 작성/삭제 도우미.
*/
private function writeState(array $config): void
{
$state = ['config' => $config];
@file_put_contents(STATE_PATH, json_encode($state));
}
private function clearState(): void
{
@unlink(STATE_PATH);
}
// ========================================================================
// Critical-1 — getComposerCommand 공백 분기 RCE 차단
// ========================================================================
public function test_get_composer_command_rejects_space_containing_input_falls_back_to_composer(): void
{
$this->writeState(['composer_binary' => 'sh -c "id > /tmp/g7_should_not_run"']);
$cmd = getComposerCommand();
$this->assertSame('composer', $cmd, '공백 포함 입력은 시스템 기본 composer 로 폴백');
$this->clearState();
}
public function test_get_composer_command_rejects_shell_metachars_falls_back_to_composer(): void
{
foreach ([
'/bin/sh; touch /tmp/x',
'/bin/composer && id',
'$(id)',
'`id`',
"/bin/composer\nrm -rf /",
] as $payload) {
$this->writeState(['composer_binary' => $payload]);
$cmd = getComposerCommand();
$this->assertSame('composer', $cmd, "셸 메타문자 포함 입력 거부 (입력: {$payload})");
}
$this->clearState();
}
public function test_get_composer_command_passes_through_safe_absolute_path(): void
{
// open_basedir 같은 PHP 런타임 제약 환경의 false negative 를 피하기 위해
// stat 의존 가드가 제거됨. 메타문자 없는 단일 절대경로는 escape 후 그대로 사용.
// 실제 실행 가능 여부는 exec 결과로 최종 판정 (silent fallback 안 함).
$this->writeState(['composer_binary' => '/usr/local/bin/composer']);
$cmd = getComposerCommand();
$this->assertSame(escapeshellarg('/usr/local/bin/composer'), $cmd);
$this->clearState();
}
public function test_get_composer_command_empty_returns_default(): void
{
$this->writeState(['composer_binary' => '']);
$this->assertSame('composer', getComposerCommand());
$this->clearState();
}
public function test_get_composer_command_for_display_does_not_leak_shell_payload(): void
{
$this->writeState(['composer_binary' => 'rm -rf /']);
$display = getComposerCommandForDisplay();
$this->assertSame('composer', $display, '셸 페이로드는 디스플레이에도 노출되지 않음');
$this->assertStringNotContainsString('rm', $display);
$this->clearState();
}
public function test_is_installer_executable_path_rejects_metachars(): void
{
// 백슬래시는 Windows 경로 구분자이므로 차단 대상 아님 (escapeshellarg 가 셸 인젝션 차단)
foreach (['foo bar', 'a;b', 'a`b', 'a$b', 'a|b', "a\nb", 'a"b', "a'b", "a\0b", "a\x01b", "a\rb"] as $bad) {
$this->assertFalse(isInstallerExecutablePath($bad), '메타문자 거부: '.bin2hex($bad));
}
}
public function test_is_installer_executable_path_accepts_windows_paths(): void
{
// Windows 절대경로는 백슬래시 포함이지만 공백·메타문자 없으면 통과.
// 공백 포함 디렉토리(예: 'C:\\Program Files\\...') 는 공백 분리 입력 형식의 토큰
// 분리 휴리스틱과 충돌하므로 본 회복 범위 외 — 알려진 한계.
foreach ([
'C:\\laragon\\bin\\php\\php-8.3.26-Win32-vs16-x64\\php.exe',
'C:\\php\\php.exe',
'D:\\xampp\\php\\php.exe',
'C:\\php\\composer.phar',
] as $path) {
$this->assertTrue(isInstallerExecutablePath($path), "Windows 경로 허용: {$path}");
}
}
public function test_is_installer_executable_path_rejects_windows_path_with_space_known_limitation(): void
{
// 'C:\\Program Files\\...' 같은 공백 포함 Windows 경로는 본 회복 범위 외.
// 공백 분리 입력(PHP + Composer 합성) 형식 휴리스틱과 충돌 — escapeshellarg 단일 토큰
// wrap 만으로는 공백 의도(토큰 구분 vs 디렉토리명) 를 자동 구분할 수 없음.
// 회피: 8.3 short path 형식(C:\\PROGRA~1\\...) 사용 또는 공백 없는 경로 사용.
$this->assertFalse(isInstallerExecutablePath('C:\\Program Files\\PHP\\php.exe'));
}
// ========================================================================
// open_basedir 회귀 회복 — stat 의존 가드 완화
// ========================================================================
/**
* stat 가드가 제거되어 시놀로지 DSM 등 open_basedir 환경에서도
* 정상 절대경로 입력이 통과해야 함. 실제 실행 가능 여부는 exec 결과로 판정.
*/
public function test_is_installer_executable_path_accepts_safe_absolute_path_without_stat(): void
{
// 실제로 존재하지 않는 경로지만 메타문자 없음 — stat 가드 제거로 true
$this->assertTrue(isInstallerExecutablePath('/usr/local/bin/php83'));
$this->assertTrue(isInstallerExecutablePath('/opt/plesk/php/8.3/bin/php'));
$this->assertTrue(isInstallerExecutablePath('/nonexistent/path/to/binary'));
}
// ========================================================================
// Composer 공백 분리 입력 안전 복원 — 멀티 PHP 환경 호환성
// ========================================================================
public function test_get_composer_command_accepts_php_composer_space_separated_input(): void
{
// 시놀로지/cPanel/Plesk 멀티 PHP 환경의 운영 패턴
$this->writeState(['composer_binary' => '/usr/local/bin/php83 /usr/local/bin/composer']);
$cmd = getComposerCommand();
// 두 토큰 각각 escapeshellarg 적용 후 공백으로 합성
$expected = escapeshellarg('/usr/local/bin/php83').' '.escapeshellarg('/usr/local/bin/composer');
$this->assertSame($expected, $cmd);
$this->clearState();
}
public function test_get_composer_command_rejects_space_separated_with_metachar_in_first_token(): void
{
// 첫 토큰에 메타문자 → 거부, composer 폴백
$this->writeState(['composer_binary' => '/usr/local/bin/php$(id) /usr/local/bin/composer']);
$cmd = getComposerCommand();
$this->assertSame('composer', $cmd);
$this->clearState();
}
public function test_get_composer_command_rejects_space_separated_with_metachar_in_second_token(): void
{
// 두 번째 토큰에 메타문자 → 거부, composer 폴백
$this->writeState(['composer_binary' => '/usr/local/bin/php83 /usr/local/bin/composer;id']);
$cmd = getComposerCommand();
$this->assertSame('composer', $cmd);
$this->clearState();
}
public function test_get_composer_command_rejects_three_or_more_tokens(): void
{
// 3 토큰 이상은 두 번째 토큰에 공백이 남음 → 두 번째 토큰의 메타문자(공백) 로 거부
$this->writeState(['composer_binary' => '/bin/php /bin/composer extra_arg']);
$cmd = getComposerCommand();
$this->assertSame('composer', $cmd);
$this->clearState();
}
public function test_get_composer_command_for_display_shows_human_friendly_space_separated_form(): void
{
$this->writeState(['composer_binary' => '/usr/local/bin/php83 /usr/local/bin/composer']);
$display = getComposerCommandForDisplay();
// 사람에게 보이는 표기는 escape 없는 원본 형식 유지
$this->assertSame('/usr/local/bin/php83 /usr/local/bin/composer', $display);
$this->clearState();
}
public function test_get_composer_command_for_display_does_not_leak_shell_payload_in_space_separated(): void
{
// 두 토큰 중 하나라도 메타문자 포함이면 display 도 composer 폴백
$this->writeState(['composer_binary' => '/bin/php `id`']);
$display = getComposerCommandForDisplay();
$this->assertSame('composer', $display);
$this->assertStringNotContainsString('`', $display);
$this->clearState();
}
// ========================================================================
// validatePhpPath / validateComposerPath stat 가드 완화 (인스톨러 검증 API)
// ========================================================================
public function test_validate_php_path_rejects_shell_metachars(): void
{
$api = new ValidationApi;
foreach ([
'/usr/local/bin/php; id',
'/usr/local/bin/php$(id)',
'`/usr/local/bin/php`',
'/usr/local/bin/php|nc evil',
"/usr/local/bin/php\nrm -rf /",
] as $payload) {
$result = $this->invokePrivate($api, 'validatePhpPath', [$payload]);
$this->assertFalse($result['valid'], "메타문자 거부: {$payload}");
}
}
public function test_validate_php_path_rejects_empty(): void
{
$api = new ValidationApi;
$result = $this->invokePrivate($api, 'validatePhpPath', ['']);
$this->assertFalse($result['valid']);
}
public function test_validate_php_path_safe_path_reaches_exec_phase(): void
{
// 메타문자 없는 절대경로는 stat 가드 없이 exec 단계까지 도달.
// 존재하지 않으면 exec 실패 (return code != 0) 로 거부 — 단, 거부 메시지는
// 'error_php_exec_failed' 로 stat 거부와 동일. 핵심: stat 가드가 사라졌다는 점.
$api = new ValidationApi;
$result = $this->invokePrivate($api, 'validatePhpPath', ['/nonexistent/path/to/php']);
// 실제 실행 실패로 invalid 이지만, 그 판정이 exec 결과에 기반함을 의미적으로 검증.
$this->assertFalse($result['valid']);
}
public function test_validate_composer_path_rejects_shell_metachars(): void
{
$api = new ValidationApi;
foreach ([
'/usr/local/bin/composer; id',
'/usr/local/bin/composer$(id)',
'`/usr/local/bin/composer`',
"/usr/local/bin/composer\nrm",
] as $payload) {
$result = $this->invokePrivate($api, 'validateComposerPath', [$payload]);
$this->assertFalse($result['valid'], "메타문자 거부: {$payload}");
}
}
public function test_validate_composer_path_rejects_space_separated_with_metachar(): void
{
// 공백 분리 입력의 토큰별 메타문자 검증
$api = new ValidationApi;
foreach ([
'/bin/php$(id) /bin/composer',
'/bin/php /bin/composer;id',
'/bin/php `id`',
'/bin/php /bin/composer extra_token',
] as $payload) {
$result = $this->invokePrivate($api, 'validateComposerPath', [$payload]);
$this->assertFalse($result['valid'], "공백 분리 + 메타문자 거부: {$payload}");
}
}
public function test_split_php_composer_tokens_helper_splits_into_two_tokens(): void
{
$api = new ValidationApi;
$result = $this->invokePrivate($api, 'splitPhpComposerTokens', ['/usr/local/bin/php83 /usr/local/bin/composer']);
$this->assertSame(['php' => '/usr/local/bin/php83', 'composer' => '/usr/local/bin/composer'], $result);
}
public function test_split_php_composer_tokens_helper_returns_null_for_single_token(): void
{
$api = new ValidationApi;
$result = $this->invokePrivate($api, 'splitPhpComposerTokens', ['/usr/local/bin/composer']);
$this->assertNull($result);
}
public function test_is_installer_safe_path_arg_helper_accepts_safe_paths(): void
{
$api = new ValidationApi;
foreach ([
'/usr/local/bin/php83',
'/opt/php/bin/php',
'C:/php/php.exe',
'/nonexistent/path',
// Windows 절대경로 (백슬래시 포함) — 회귀 가드: Windows 빌트인 서버 환경
'C:\\laragon\\bin\\php\\php-8.3.26-Win32-vs16-x64\\php.exe',
'C:\\php\\php.exe',
'D:\\xampp\\php\\php.exe',
] as $safe) {
$this->assertTrue($this->invokePrivate($api, 'isInstallerSafePathArg', [$safe]), "허용: {$safe}");
}
}
public function test_is_installer_safe_path_arg_helper_rejects_metachars_and_empty(): void
{
$api = new ValidationApi;
$this->assertFalse($this->invokePrivate($api, 'isInstallerSafePathArg', ['']));
// 백슬래시는 Windows 경로 구분자이므로 차단 대상이 아님 — 셸 인젝션 차단은 escapeshellarg 가 담당
foreach (['foo bar', 'a;b', 'a`b', 'a$b', 'a|b', "a\nb", 'a"b', "a'b", "a\0b", "a\x01b", "a\rb"] as $bad) {
$this->assertFalse($this->invokePrivate($api, 'isInstallerSafePathArg', [$bad]), '메타문자 거부: '.bin2hex($bad));
}
}
// ========================================================================
// 워커 정합 — 검증 통과한 입력이 워커에서도 silent fallback 없이 전달
// ========================================================================
public function test_get_composer_command_passes_through_single_absolute_path_without_silent_fallback(): void
{
// 검증 단계에서 통과한 단일 절대경로가 워커에서도 그대로 사용됨을 보장.
// stat 가드 제거 후 회귀 — 정상 입력이 silent fallback 으로 사라지지 않음.
$this->writeState(['composer_binary' => '/usr/local/bin/composer']);
$cmd = getComposerCommand();
$this->assertNotSame('composer', $cmd, '검증 통과한 입력은 silent fallback 안 됨');
$this->assertSame(escapeshellarg('/usr/local/bin/composer'), $cmd);
$this->clearState();
}
// ========================================================================
// High-2 — checkCorePendingPath traversal 차단
// ========================================================================
public function test_check_core_pending_path_rejects_parent_traversal(): void
{
$api = new ValidationApi;
foreach (['../../../etc', '..', 'foo/../bar', 'foo/..\\bar'] as $bad) {
$_GET = ['path' => $bad];
ob_start();
$this->invokePrivate($api, 'checkCorePendingPath', []);
$body = ob_get_clean();
$decoded = json_decode($body, true);
$this->assertIsArray($decoded);
$this->assertFalse($decoded['success'], "traversal 거부: {$bad}");
// 단일 통일 메시지로 enumeration 신호 차단
$this->assertSame('error_core_pending_path_invalid', $decoded['message']);
}
$_GET = [];
}
public function test_check_core_pending_path_rejects_null_byte(): void
{
$api = new ValidationApi;
$_GET = ['path' => "valid\0../etc"];
ob_start();
$this->invokePrivate($api, 'checkCorePendingPath', []);
$body = ob_get_clean();
$decoded = json_decode($body, true);
$this->assertFalse($decoded['success']);
$_GET = [];
}
public function test_check_core_pending_path_returns_uniform_message_for_nonexistent(): void
{
$api = new ValidationApi;
$_GET = ['path' => '/nonexistent/g7-test-'.bin2hex(random_bytes(4))];
ob_start();
$this->invokePrivate($api, 'checkCorePendingPath', []);
$body = ob_get_clean();
$decoded = json_decode($body, true);
$this->assertFalse($decoded['success']);
// 존재 여부/타입 차이를 응답에 노출하지 않음
$this->assertSame('error_core_pending_path_invalid', $decoded['message']);
$_GET = [];
}
// ========================================================================
// Medium-1 — escapeEnvValue 개행 거부
// ========================================================================
//
// 종전에는 개행을 조용히 지웠다(Medium-1). KVE-2026-2042 정정으로 직렬화기는 개행을
// 만나면 예외를 던진다 — 지우면 운영자가 입력한 값과 저장된 값이 달라지는데 그 사실이
// 화면에 나타나지 않기 때문이다. 하위호환 별칭 escapeEnvValue 도 같은 관문에 위임한다.
public function test_escape_env_value_rejects_newlines(): void
{
$this->expectException(InvalidArgumentException::class);
escapeEnvValue("secret\nINJECTED=true");
}
public function test_escape_env_value_rejects_crlf(): void
{
$this->expectException(InvalidArgumentException::class);
escapeEnvValue("a\r\nb");
}
public function test_escape_env_value_preserves_normal_password(): void
{
$result = escapeEnvValue('p@ssw0rd!#$%');
$this->assertSame('"p@ssw0rd!#$%"', $result);
}
public function test_escape_env_value_escapes_quotes_and_backslashes(): void
{
$result = escapeEnvValue('a"b\\c');
$this->assertSame('"a\\"b\\\\c"', $result);
}
// ========================================================================
// Medium-2 — PDO DSN 파라미터 sanitize
// ========================================================================
public function test_get_database_connection_rejects_semicolon_in_host(): void
{
$this->expectException(\PDOException::class);
getDatabaseConnection([
'db_write_host' => 'localhost;injected=evil',
'db_write_port' => '3306',
'db_write_database' => 'testdb',
'db_write_username' => 'root',
'db_write_password' => '',
], false);
}
public function test_get_database_connection_rejects_equals_in_database(): void
{
$this->expectException(\PDOException::class);
getDatabaseConnection([
'db_write_host' => 'localhost',
'db_write_port' => '3306',
'db_write_database' => 'test=evil',
'db_write_username' => 'root',
'db_write_password' => '',
], false);
}
public function test_get_database_connection_rejects_newline_in_port(): void
{
$this->expectException(\PDOException::class);
getDatabaseConnection([
'db_write_host' => 'localhost',
'db_write_port' => "3306\nfoo=bar",
'db_write_database' => 'testdb',
'db_write_username' => 'root',
'db_write_password' => '',
], false);
}
// ========================================================================
// KVE-2026-1677 — 설치 워커의 실행 바이너리 경로
// ========================================================================
/**
* 저장된 php_binary 가 형태 규칙을 위반하면 시스템 기본값으로 폴백한다.
*
* 이 값은 설치 워커에서 실제 명령의 실행 바이너리가 된다(`--version` 이 붙지 않는
* 호출 지점). 이전에는 아무 검증 없이 그대로 반환해, 저장만 되면 인자 자리가 열렸다.
*
* @scenario vector=getPhpBinary, payload_class=argument_injection_dash_r
*
* @effects getPhpBinary_falls_back_to_default_for_disallowed_shape
*/
public function test_get_php_binary_falls_back_to_default_for_disallowed_shape(): void
{
foreach ([
'-rphpinfo();' => '코드 실행 플래그',
'-dauto_prepend_file=/tmp/x' => 'ini 주입',
'bin/php' => '상대경로',
'/usr/local/../../tmp/php' => '.. 세그먼트',
'/usr/bin/php$(id)' => '셸 메타문자',
] as $payload => $reason) {
$this->writeState(['php_binary' => $payload]);
$this->assertSame(
'php',
getPhpBinary(),
"형태 위반 값이 그대로 실행 바이너리가 됨 ({$reason}): {$payload}"
);
}
}
/**
* 정상 절대경로는 그대로 사용된다 (stat 미사용 — 존재하지 않아도 통과).
*
* @scenario vector=getPhpBinary, payload_class=wrapper_script_name
*
* @effects getPhpBinary_passes_through_legitimate_paths
*/
public function test_get_php_binary_passes_through_legitimate_paths(): void
{
foreach ([
'/usr/local/php84/bin/php',
'/usr/local/bin/php-8.2-wrapper.sh',
'C:\\php\\php.exe',
'/usr/local/php99/bin/php',
] as $path) {
$this->writeState(['php_binary' => $path]);
$this->assertSame($path, getPhpBinary(), "정상 경로가 폴백됨: {$path}");
}
}
/**
* Composer 자리에 인터프리터 + 임의 스크립트를 넣으면 기본값으로 폴백한다.
*
* 하이픈 선두만 막으면 `PHP경로 /tmp/올려둔파일` 형태가 남는다.
*
* @scenario vector=getComposerCommand, payload_class=interpreter_plus_script
*
* @effects getComposerCommand_rejects_interpreter_plus_arbitrary_script
*/
public function test_get_composer_command_rejects_interpreter_plus_arbitrary_script(): void
{
foreach ([
'/usr/bin/php /tmp/evil.php',
'/usr/bin/python3 /tmp/evil.py',
'/usr/bin/php -rphpinfo();',
'/usr/bin/php /tmp/composer-evil.sh',
] as $payload) {
$this->writeState(['composer_binary' => $payload]);
$this->assertSame(
'composer',
getComposerCommand(),
"인자 자리에 임의 스크립트/옵션이 통과함: {$payload}"
);
}
}
/**
* 정상 멀티 PHP 입력은 계속 두 토큰으로 실행된다 (비파괴).
*
* @scenario vector=getComposerCommand, payload_class=wrapper_script_name
*
* @effects getComposerCommand_keeps_multi_php_pair_support
*/
public function test_get_composer_command_keeps_multi_php_pair_support(): void
{
$this->writeState(['composer_binary' => '/usr/local/bin/php-8.2-wrapper.sh /usr/local/bin/composer.phar']);
$cmd = getComposerCommand();
$this->assertStringContainsString('php-8.2-wrapper.sh', $cmd);
$this->assertStringContainsString('composer.phar', $cmd);
}
}