|null $allowedExtensions 허용 확장자 (null 이면 확장자 검사 생략) */ public function __construct(private readonly ?array $allowedExtensions = null) {} /** * 경로가 안전한지 검증합니다. * * @param string $attribute 속성명 * @param mixed $value 값 * @param Closure $fail 실패 콜백 * @return void */ public function validate(string $attribute, mixed $value, Closure $fail): void { if (! is_string($value)) { $fail(__('custom_assets.errors.invalid_path', ['path' => ''])); return; } $normalized = str_replace('\\', '/', trim($value)); if ($normalized === '' || str_starts_with($normalized, '/')) { $fail(__('custom_assets.errors.invalid_path', ['path' => $value])); return; } foreach (explode('/', $normalized) as $segment) { if ($segment === '' || $segment === '.' || $segment === '..') { $fail(__('custom_assets.errors.invalid_path', ['path' => $value])); return; } } if ($this->allowedExtensions === null) { return; } $extension = strtolower(pathinfo($normalized, PATHINFO_EXTENSION)); if (! in_array($extension, $this->allowedExtensions, true)) { $fail(__('custom_assets.errors.extension_not_allowed', ['extension' => $extension])); } } }