assertNull( $violation, '정상 언어팩 파일이 거부됨: '.($violation['reason'] ?? '').' — '.$source ); } /** * 번역 배열 외의 문법은 전부 거부한다. * * 기존 블록리스트가 놓치던 함수들(`file_put_contents` `fopen` `call_user_func` `assert` * 백틱)과, 함수 이름을 아예 쓰지 않는 우회(문자열 리터럴 호출·가변 변수·heredoc· * 닫는 태그 뒤 인라인 코드)를 같은 규칙 하나로 막는지 확인한다. * * @param string $source PHP 소스 * @param string $vector 우회 수법 (실패 메시지용) * * @scenario vector=php_syntax_validator, actor_permission=cli_system * * @effects function_calls_are_rejected_regardless_of_name, name_free_bypasses_are_rejected */ #[Test] #[DataProvider('unsafeSourceProvider')] public function it_rejects_anything_that_is_not_a_translation_array(string $source, string $vector): void { $this->assertFalse( LanguagePackPhpArrayValidator::isSafe($source), "차단되어야 하는 소스가 통과함 ({$vector}): {$source}" ); } /** * 깨진 소스는 파싱 단계에서 조기 거부한다. * * @scenario vector=php_syntax_validator, actor_permission=cli_system * * @effects broken_and_oversized_sources_are_rejected_early */ #[Test] public function it_rejects_sources_that_do_not_parse(): void { $violation = LanguagePackPhpArrayValidator::inspect('assertNotNull($violation); $this->assertSame('parse_error', $violation['reason']); } /** * 크기 상한을 넘는 소스는 파서를 돌리지 않고 거부한다. */ #[Test] public function it_rejects_sources_over_the_size_limit(): void { $source = ' 'b', ", 200000).'];'; $this->assertGreaterThan(LanguagePackPhpArrayValidator::MAX_BYTES, strlen($source)); $violation = LanguagePackPhpArrayValidator::inspect($source); $this->assertNotNull($violation); $this->assertSame('file_too_large', $violation['reason']); } /** * 중첩 깊이 상한을 넘는 배열은 거부한다. */ #[Test] public function it_rejects_arrays_deeper_than_the_depth_limit(): void { $depth = LanguagePackPhpArrayValidator::MAX_DEPTH + 5; $source = 'assertFalse(LanguagePackPhpArrayValidator::isSafe($source)); } /** * 위반 지점의 줄 번호를 보고한다 (운영자가 어느 줄을 고쳐야 하는지 알 수 있어야 한다). * * @scenario vector=php_syntax_validator, actor_permission=cli_system * * @effects violation_line_is_reported */ #[Test] public function it_reports_the_line_of_the_violation(): void { $source = " 'b',\n 'c' => system('id'),\n];\n"; $violation = LanguagePackPhpArrayValidator::inspect($source); $this->assertNotNull($violation); $this->assertSame(5, $violation['line']); } /** * 통과해야 하는 소스 목록. * * @return array */ public static function safeSourceProvider(): array { return [ '빈 배열' => [" [" 'こんにちは'];\n"], '중첩 배열' => [" ['b' => ['c' => 'd']]];\n"], '숫자 키' => [" 'zero', 1 => 'one'];\n"], '키 없는 항목' => [" [" 'b',\n];\n"], 'declare 프리픽스' => [" 'b'];\n"], '문자열 리터럴 연결' => [" 'long '.'message'];\n"], '숫자·bool·null' => [" 1, 'f' => 1.5, 'neg' => -3, 't' => true, 'f2' => false, 'x' => null];\n"], '주석 포함' => [" 'b', // 항목\n];\n"], 'array() 레거시 문법' => [" 'b');\n"], '닫는 태그 포함' => [" 'b'];\n?>\n"], '이스케이프 포함 문자열' => [" 'It\\'s ok', 'b' => \"tab\\there\"];\n"], '대문자 상수 표기' => [" TRUE, 'b' => NULL];\n"], ]; } /** * 차단되어야 하는 소스 목록. * * @return array */ public static function unsafeSourceProvider(): array { return [ // 기존 블록리스트가 잡던 것 'eval' => [" [" [" [" [" [" [" [" file_get_contents('/etc/passwd')];\n", '목록 누락 — 값 자리 호출'], // 함수 이름을 쓰지 않는 우회 (블록리스트로는 원리적으로 불가) '문자열 리터럴 호출' => [" [" [" \$GLOBALS['x']];\n", '변수'], 'heredoc 보간' => [" ["\n ["\nleaked\n", '닫는 태그 뒤 인라인 출력'], 'new 객체' => [" new \\stdClass];\n", 'new'], '상수 참조' => [" PHP_OS];\n", '상수 — 정보 노출'], '삼항 연산' => [" 1 > 0 ? 'y' : 'n'];\n", '연산자'], '함수 선언' => [" [" [" [" [" ["\n", '단축 태그'], '선행 인라인 HTML' => ["leaked\n [" [" [" [" 'x'];\n", '키 자리 호출'], ]; } }