diff --git a/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php b/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php index 2b2fa2b..29a9ac2 100644 --- a/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php +++ b/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php @@ -7,6 +7,7 @@ use Automattic\BlocksEngine\PhpTransformer\Contract\TransformerResult; use Automattic\BlocksEngine\PhpTransformer\FormatBridge\FormatBridge; use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\HtmlTransformer; +use Automattic\BlocksEngine\PhpTransformer\Path\ArtifactPath; use Automattic\BlocksEngine\PhpTransformer\StaticSite\MaterializationPlanBuilder; final class ArtifactCompiler @@ -1066,29 +1067,7 @@ private function findAssetByHtmlReference(string $reference, string $entryPath, private function resolveHtmlReferencePath(string $reference, string $entryPath): string { - $reference = strtok($reference, '?#') ?: ''; - $reference = str_replace('\\', '/', trim($reference)); - if ( '' === $reference || str_starts_with($reference, '/') || preg_match('#^[a-z][a-z0-9+.-]*:#i', $reference) ) { - return ''; - } - - $base = '' === $entryPath || ! str_contains($entryPath, '/') ? '' : dirname($entryPath) . '/'; - $parts = array(); - foreach ( explode('/', $base . $reference) as $part ) { - if ( '' === $part || '.' === $part ) { - continue; - } - if ( '..' === $part ) { - if ( array() === $parts ) { - return ''; - } - array_pop($parts); - continue; - } - $parts[] = $part; - } - - return implode('/', $parts); + return ArtifactPath::resolveRelativePath($reference, $entryPath); } /** @@ -1345,8 +1324,7 @@ private function resolveComponentImport(string $importPath, string $sourcePath, return ''; } - $base = dirname($sourcePath); - $path = $this->normalizeRelativeImportPath(('.' === $base ? '' : $base . '/') . $importPath); + $path = ArtifactPath::resolveRelativePath($importPath, $sourcePath, true); if ( '' === $path ) { return ''; } @@ -1366,23 +1344,6 @@ private function resolveComponentImport(string $importPath, string $sourcePath, return ''; } - private function normalizeRelativeImportPath(string $path): string - { - $segments = array(); - foreach ( explode('/', str_replace('\\', '/', $path)) as $segment ) { - if ( '' === $segment || '.' === $segment ) { - continue; - } - if ( '..' === $segment ) { - array_pop($segments); - continue; - } - $segments[] = preg_replace('/[^A-Za-z0-9._-]/', '-', $segment); - } - - return implode('/', array_filter($segments)); - } - /** * @param array $frontmatter * @param array $keys diff --git a/php-transformer/src/ArtifactCompiler/ArtifactNormalizer.php b/php-transformer/src/ArtifactCompiler/ArtifactNormalizer.php index edc5e2e..426a65a 100644 --- a/php-transformer/src/ArtifactCompiler/ArtifactNormalizer.php +++ b/php-transformer/src/ArtifactCompiler/ArtifactNormalizer.php @@ -3,6 +3,8 @@ namespace Automattic\BlocksEngine\PhpTransformer\ArtifactCompiler; +use Automattic\BlocksEngine\PhpTransformer\Path\ArtifactPath; + /** * Normalizes loose website artifact envelopes into compiler-ready file records. * @@ -43,7 +45,7 @@ public function normalize(array $artifact): array $rawFiles = $this->rawFiles($artifact); $safeEntrypoints = array(); foreach ( array_unique($entrypoints) as $entrypoint ) { - $path = $this->safeRelativePath($entrypoint); + $path = ArtifactPath::safeRelativePath($entrypoint); if ( '' === $path ) { $diagnostics[] = $this->diagnostic('unsafe_entrypoint_path', 'warning', 'An artifact entrypoint was ignored because its path is empty, absolute, or escapes the artifact root.', array('path' => $entrypoint)); continue; @@ -58,7 +60,7 @@ public function normalize(array $artifact): array break; } - $path = $this->safeRelativePath((string) ($file['path'] ?? '')); + $path = ArtifactPath::safeRelativePath((string) ($file['path'] ?? '')); if ( '' === $path ) { ++$rejected; $diagnostics[] = $this->diagnostic('unsafe_artifact_path', 'warning', 'An artifact file was ignored because its path is empty, absolute, or escapes the artifact root.', array('index' => $index)); @@ -227,26 +229,6 @@ private function payload(array $file, string $path): array return array('accepted' => true, 'content' => $content, 'content_base64' => '', 'encoding' => 'text', 'binary' => false, 'bytes' => strlen($content), 'diagnostics' => array()); } - private function safeRelativePath(string $path): string - { - $path = str_replace('\\', '/', trim($path)); - if ( '' === $path || str_starts_with($path, '/') || preg_match('#^[A-Za-z]:/#', $path) ) { - return ''; - } - $parts = array(); - foreach ( explode('/', $path) as $part ) { - if ( '' === $part || '.' === $part ) { - continue; - } - if ( '..' === $part ) { - return ''; - } - $parts[] = $part; - } - - return implode('/', $parts); - } - private function kind(string $kind, string $path, string $content, string $mimeType): string { $kind = $this->sanitizeKey($kind); diff --git a/php-transformer/src/Path/ArtifactPath.php b/php-transformer/src/Path/ArtifactPath.php new file mode 100644 index 0000000..16ddaae --- /dev/null +++ b/php-transformer/src/Path/ArtifactPath.php @@ -0,0 +1,70 @@ +transform($fixture . "\n")->toArray(); @@ -261,14 +270,17 @@ function serialize_blocks(array $blocks): string array( 'entrypoints' => array('../unsafe.html'), 'files' => array( - '../secret.html' => '
Nope
', - 'safe.html' => '
Safe
', + '../secret.html' => '
Nope
', + '/absolute.html' => '
Nope
', + 'safe.html' => '
Safe
', + 'assets//nested/style.css' => '.safe{}', ), ) )->toArray(); $assert('success_with_warnings' === $unsafe['status'], 'unsafe paths produce warning status', (string) $unsafe['status']); -$assert(1 === ($unsafe['source_reports']['artifact']['rejected_count'] ?? null), 'unsafe paths are rejected'); +$assert(2 === ($unsafe['source_reports']['artifact']['rejected_count'] ?? null), 'unsafe paths are rejected'); $assert('unsafe_entrypoint_path' === ($unsafe['diagnostics'][0]['code'] ?? ''), 'unsafe entrypoints are diagnosed'); +$assert(! empty(array_filter($unsafe['assets'], static fn (array $asset): bool => 'assets/nested/style.css' === ($asset['path'] ?? ''))), 'safe artifact paths collapse duplicate separators'); $binary = $compiler->compile( array(