@@ -274,7 +274,7 @@ private function createExecutionPlan( array $blueprint ): array {
274274 if ( isset ( $ blueprint ['activeTheme ' ] ) ) {
275275 $ themeRef = $ blueprint ['activeTheme ' ];
276276 if ( is_string ( $ themeRef ) ) {
277- $ plan [] = [
277+ $ step = [
278278 'name ' => 'installTheme ' ,
279279 'args ' => [
280280 'source ' => $ themeRef ,
@@ -283,9 +283,9 @@ private function createExecutionPlan( array $blueprint ): array {
283283 ]
284284 ];
285285 } elseif ( is_array ( $ themeRef ) && isset ( $ themeRef ['source ' ] ) && is_string ( $ themeRef ['source ' ] ) ) {
286- $ plan [] = [
286+ $ step = [
287287 'name ' => 'installTheme ' ,
288- 'args ' => [
288+ 'args ' => [
289289 'source ' => $ themeRef ['source ' ],
290290 'active ' => true ,
291291 'importStarterContent ' => $ themeRef ['importStarterContent ' ] ?? false ,
@@ -295,6 +295,12 @@ private function createExecutionPlan( array $blueprint ): array {
295295 } else {
296296 throw new InvalidArgumentException ( 'Invalid theme reference format for "activeTheme". ' );
297297 }
298+
299+ $ plan [] = $ step ;
300+ $ error = $ this ->validateDataSource ( $ step ['args ' ]['source ' ], 'wp-content/themes/ ' );
301+ if ( $ error ) {
302+ $ errors ['themes ' ] = [$ error ];
303+ }
298304 }
299305
300306 // 6. plugins
@@ -309,6 +315,11 @@ private function createExecutionPlan( array $blueprint ): array {
309315 'name ' => 'installPlugin ' ,
310316 'args ' => $ pluginDef
311317 ];
318+
319+ $ error = $ this ->validateDataSource ( $ pluginDef ['source ' ], 'wp-content/plugins/ ' );
320+ if ( $ error ) {
321+ $ errors ['plugins ' ] = [$ error ];
322+ }
312323 }
313324 }
314325
@@ -410,4 +421,33 @@ private function createExecutionPlan( array $blueprint ): array {
410421
411422 return [ $ plan , $ errors ];
412423 }
424+
425+ private function validateDataSource ( string $ source , string $ bundle_prefix ): ?string {
426+
427+ if ( strlen ( $ source ) === 0 ) {
428+ return 'Source must be a non-empty string. ' ;
429+ }
430+
431+ // 1. Absolute URL.
432+ if ( str_contains ( $ source , ':// ' ) ) {
433+ return null ;
434+ }
435+
436+ // 2. Bundle-relative path.
437+ $ byte_1 = $ source [0 ];
438+ $ byte_2 = $ source [1 ] ?? null ;
439+ if ( str_contains ( $ source , '/ ' ) ) {
440+ if ( $ byte_1 === '/ ' ) {
441+ $ source = substr ( $ source , 1 );
442+ } elseif ( $ byte_1 === '. ' && $ byte_2 === '/ ' ) {
443+ $ source = substr ( $ source , 2 );
444+ }
445+
446+ if ( ! str_starts_with ( $ source , $ bundle_prefix ) ) {
447+ return 'Source must be a relative path starting with " ' . $ bundle_prefix . '". ' ;
448+ }
449+ }
450+
451+ return null ;
452+ }
413453}
0 commit comments