fix(release): 7.0.7 회귀 검수 발견 결함 3건 수정 — 표현식 배열 구조 분해·스케줄 확장 명령 웹 검증·JSON-LD 이스케이프

- 표현식 평가기가 화살표/함수 파라미터의 평탄한 배열 구조 분해(([k, v]) =>, 엘리전 홀)를
 지원하지 않아 배포 레이아웃 60여 파일(게시판 권한 computed·검증 오류 표시)이 평가 실패하던
 7.0.7 평가기 교체 회귀 수정 (engine-v1.60.5) + --production 재빌드.
- 스케줄 저장 검증이 HTTP 문맥에서 확장 커맨드를 해석하지 못하던 문제(확장 프로바이더의
 runningInConsole 등록 게이트로 Artisan 레지스트리 부재)를 프로바이더 $commands 선언 폴백으로
 수정 — 판정 기준(클래스 네임스페이스 + 실제 명령명 대조) 동일, fail-closed 유지.
- 봇 렌더 JSON-LD json_encode 에 JSON_HEX_TAG 추가 — 사용자 입력(검색어·게시글 제목)의
 </script> 조기 종료로 인한 저장형 XSS 차단, schema.org 데이터는 무손실.
- SeoMetaResolverTest 의 og_default_site_name 미고정 환경의존 실패를 setUp 고정으로 격리.
- 루트 CHANGELOG 7.0.7 Security/Fixed 각 1건(기출시 결함만), 시나리오 매니페스트 2건 갱신.
This commit is contained in:
HeuJung committed 2026-08-18 10:00:01 +09:00
1 parent b3d0da9355
commit a1e1706ddc
13 files changed
+359 -28

No files matched your search

+78 -5
View File
@@ -591,15 +591,88 @@ class ScheduleCommandValidator
$command = self::findRegisteredCommand($name);
if ($command === null) {
if ($command !== null) {
$class = get_class($command);
foreach ($namespaces as $namespace) {
if (str_starts_with($class, $namespace)) {
return $command->getDefinition();
}
}
return null;
}
$class = get_class($command);
// 레지스트리에 없으면 프로바이더 선언 폴백 — HTTP 요청에서는 확장 프로바이더가
// `runningInConsole()` 게이트로 커맨드 등록을 건너뛰어 `Artisan::all()` 에
// 확장 커맨드가 존재하지 않는다. 관리자 화면의 스케줄 저장 검증이 바로 이
// 문맥이므로, 등록 인스턴스가 없을 때는 활성 프로바이더의 `$commands` 선언을
// 같은 기준(클래스 네임스페이스 + 실제 명령명 대조)으로 해석한다.
return self::resolveProviderDeclaredCommandDefinition($name, $namespaces);
}
foreach ($namespaces as $namespace) {
if (str_starts_with($class, $namespace)) {
return $command->getDefinition();
/**
* 활성 서비스 프로바이더가 `$commands` 프로퍼티로 선언한 확장 커맨드에서
* 명령명을 해석해 정의를 돌려줍니다.
*
* 판정 기준은 레지스트리 경로와 동일하다 — 커맨드 **클래스**의 네임스페이스가
* 확장 네임스페이스여야 하고, 요청된 이름은 그 클래스를 실제 인스턴스화해 얻은
* 선언 명령명과 일치해야 한다(이름만으로 위조할 수 없다). 프로바이더가 커맨드
* 목록을 생성자에서 동적으로 만들면 기본값이 비므로 이 폴백에는 걸리지 않는다
* (fail-closed 유지).
*
* @param string $name 명령명
* @param array<int, string> $namespaces 허용 확장 네임스페이스 접두사
* @return InputDefinition|null 해석 불가하면 null
*/
private static function resolveProviderDeclaredCommandDefinition(string $name, array $namespaces): ?InputDefinition
{
try {
$providers = app()->getLoadedProviders();
} catch (Throwable) {
return null;
}
foreach (array_keys($providers) as $providerClass) {
if (! class_exists($providerClass)) {
continue;
}
try {
$defaults = (new \ReflectionClass($providerClass))->getDefaultProperties();
} catch (Throwable) {
continue;
}
foreach ((array) ($defaults['commands'] ?? []) as $commandClass) {
if (! is_string($commandClass) || ! class_exists($commandClass)) {
continue;
}
$owned = false;
foreach ($namespaces as $namespace) {
if (str_starts_with($commandClass, $namespace)) {
$owned = true;
break;
}
}
if (! $owned) {
continue;
}
try {
/** @var SymfonyCommand $instance */
$instance = app()->make($commandClass);
} catch (Throwable) {
continue;
}
if (! $instance instanceof SymfonyCommand || $instance->getName() !== $name) {
continue;
}
return $instance->getDefinition();
}
}