diff --git a/src/Flame/Database/Attach/MediaAdder.php b/src/Flame/Database/Attach/MediaAdder.php index fb37adaa..aff092ee 100644 --- a/src/Flame/Database/Attach/MediaAdder.php +++ b/src/Flame/Database/Attach/MediaAdder.php @@ -21,7 +21,7 @@ class MediaAdder protected ?Model $performedOn = null; - protected string $tag = 'default'; + protected ?string $tag = 'default'; protected ?string $diskName = null; @@ -58,7 +58,7 @@ public function useDisk(?string $disk): self public function useMediaTag(?string $tag = null): self { - $this->tag = $tag ?? 'default'; + $this->tag = $tag; return $this; } @@ -71,7 +71,7 @@ public function fromFile(UploadedFile|SymfonyFile $file): Media $media->name = $media->getUniqueName(); $media->disk = $this->diskName ?? $media->getDiskName(); - $media->tag = $this->performedOn->getDefaultTagName() ?? $this->tag; + $media->tag = $this->tag ?? $this->performedOn->getDefaultTagName(); $media->custom_properties = $this->customProperties; $this->attachMedia($media); diff --git a/src/Flame/Pagic/PagicServiceProvider.php b/src/Flame/Pagic/PagicServiceProvider.php index d9625087..fc10378a 100644 --- a/src/Flame/Pagic/PagicServiceProvider.php +++ b/src/Flame/Pagic/PagicServiceProvider.php @@ -44,6 +44,8 @@ public function register(): void $this->app->singleton(FileCache::class, fn(): FileCache => new FileCache(config('igniter-pagic.parsedTemplateCachePath'))); + $this->app->singleton(TemplateSandbox::class); + $this->app->alias('pagic', Environment::class); $this->app->singleton('pagic', fn(): Environment => new Environment(new Loader, [ diff --git a/src/Flame/Pagic/TemplateSandbox.php b/src/Flame/Pagic/TemplateSandbox.php new file mode 100644 index 00000000..512235a8 --- /dev/null +++ b/src/Flame/Pagic/TemplateSandbox.php @@ -0,0 +1,149 @@ +removeNullBytes($template); + $template = $this->removePhpTags($template); + $template = $this->removePhpBlocks($template); + $template = $this->removeUnsafeBladeDirectives($template); + $template = $this->removeUnescapedOutput($template); + $template = $this->removeObfuscation($template); + $template = $this->removeVariableVariables($template); + + return $this->removePathTraversal($template); + } + + protected function removeNullBytes(string $content): string + { + return str_replace("\0", '', $content); + } + + protected function removePhpTags(string $content): string + { + // With closing tag + $content = preg_replace('/<\?(?:php|=).*?\?>/si', '', $content) ?? $content; + + // Without closing tag - rest of content after opening tag + $content = preg_replace('/<\?(?:php|=)[\s\S]*$/i', '', $content) ?? $content; + + // Short open tags (but preserve '], '', $content); + + return $content; + } + + protected function removePhpBlocks(string $content): string + { + $patterns = [ + // @php ... @endphp blocks + '/@php\b.*?@endphp/si', + // @inject directive + '/@inject\s*\([^)]*\)/i', + // Dangerous includes that load arbitrary files + '/@include[a-zA-Z]*\s*\([^)]*\)/i', + '/@require[a-zA-Z]*\s*\([^)]*\)/i', + // Layout directives that could load arbitrary files + '/@extends\s*\([^)]*\)/i', + // Component loading + '/@component[a-zA-Z]*\s*\([^)]*\)/i', + '/@livewire[a-zA-Z]*\s*\([^)]*\)/i', + ]; + + foreach ($patterns as $pattern) { + $content = preg_replace($pattern, '', $content) ?? $content; + } + + return $content; + } + + protected function removeUnsafeBladeDirectives(string $content): string + { + $dangerous = [ + // Dangerous PHP functions that could appear in Blade expressions + '/\beval\s*\([^)]*\)/i', + '/\bsystem\s*\([^)]*\)/i', + '/\bexec\s*\([^)]*\)/i', + '/\bshell_exec\s*\([^)]*\)/i', + '/\bpassthru\s*\([^)]*\)/i', + '/\bpopen\s*\([^)]*\)/i', + '/\bproc_open\s*\([^)]*\)/i', + '/\bfile_put_contents\s*\([^)]*\)/i', + '/\bfile_get_contents\s*\([^)]*\)/i', + '/\breadfile\s*\([^)]*\)/i', + '/\bfile\s*\([^)]*\)/i', + '/\bscandir\s*\([^)]*\)/i', + '/\bglob\s*\([^)]*\)/i', + '/\bbase64_decode\s*\([^)]*\)/i', + '/\bstr_rot13\s*\([^)]*\)/i', + '/\bgzinflate\s*\([^)]*\)/i', + '/\bgzuncompress\s*\([^)]*\)/i', + '/\bgzdecode\s*\([^)]*\)/i', + '/\bpreg_replace\s*\(\s*[\'"].*?e[\'"]/i', + '/\bcreate_function\s*\([^)]*\)/i', + '/\bassert\s*\([^)]*\)/i', + '/\bphpinfo\s*\([^)]*\)/i', + '/\bgetallheaders\s*\([^)]*\)/i', + '/\bheader\s*\([^)]*\)/i', + '/\bsetcookie\s*\([^)]*\)/i', + '/\bmove_uploaded_file\s*\([^)]*\)/i', + '/\bunlink\s*\([^)]*\)/i', + '/\brmdir\s*\([^)]*\)/i', + '/\bmkdir\s*\([^)]*\)/i', + '/\bchmod\s*\([^)]*\)/i', + '/\bchown\s*\([^)]*\)/i', + ]; + + foreach ($dangerous as $pattern) { + $content = preg_replace($pattern, '', $content) ?? $content; + } + + return $content; + } + + protected function removeUnescapedOutput(string $content): string + { + // {!! unescaped !!} - force all output through Blade's escaping + return preg_replace('/\{!!\s*.+?\s*!!\}/s', '', $content) ?? $content; + } + + protected function removeObfuscation(string $content): string + { + $patterns = [ + '/\\\\x[0-9a-fA-F]{2}/i', // \x47 hex encoding + '/\\\\u[0-9a-fA-F]{4}/i', // \u0047 unicode + '/chr\s*\(\s*\d+\s*\)/i', // chr(72) char concatenation + '/GLOBALS\s*\[[^\]]*\]/i', // $GLOBALS['var'] + ]; + + foreach ($patterns as $pattern) { + $content = preg_replace($pattern, '', $content) ?? $content; + } + + return $content; + } + + protected function removeVariableVariables(string $content): string + { + // $$var and ${...} variable variables + $content = preg_replace('/\$\$[a-zA-Z_\x7f-\xff]/i', '', $content) ?? $content; + + return preg_replace('/\$\{[^}]*\}/i', '', $content) ?? $content; + } + + protected function removePathTraversal(string $content): string + { + $content = preg_replace('/\.\.\//i', '', $content) ?? $content; + + return preg_replace('/\.\.\\\\/i', '', $content) ?? $content; + } +} diff --git a/src/Flame/Support/Igniter.php b/src/Flame/Support/Igniter.php index 13c43bf5..2827d8c1 100644 --- a/src/Flame/Support/Igniter.php +++ b/src/Flame/Support/Igniter.php @@ -13,7 +13,7 @@ class Igniter { - protected const string VERSION = 'v4.2.2'; + protected const string VERSION = 'v4.2.3'; /** * The base path for extensions. diff --git a/src/Main/FormWidgets/TemplateEditor.php b/src/Main/FormWidgets/TemplateEditor.php index e8fa5022..5d184098 100644 --- a/src/Main/FormWidgets/TemplateEditor.php +++ b/src/Main/FormWidgets/TemplateEditor.php @@ -11,6 +11,7 @@ use Igniter\Admin\Widgets\Form; use Igniter\Flame\Exception\FlashException; use Igniter\Flame\Pagic\Model; +use Igniter\Flame\Pagic\TemplateSandbox; use Igniter\Main\Classes\Theme; use Igniter\Main\Classes\ThemeManager; use Illuminate\Contracts\Validation\Validator; @@ -260,14 +261,19 @@ protected function getTemplateTypes(): array protected function getTemplateAttributes(): array { + $templateSanitizer = resolve(TemplateSandbox::class); $formData = $this->templateWidget?->getSaveData() ?? []; $code = (string)array_get($formData, 'codeSection', ''); $code = preg_replace('/^\<\?php/', '', $code); $code = preg_replace('/^\<\?/', '', (string)preg_replace('/\?>$/', '', (string)$code)); - $result['code'] = trim((string)$code, PHP_EOL) ?: null; - $result['markup'] = array_get($formData, 'markup') ?: null; + $result['code'] = trim((string)$code, PHP_EOL) !== '' && trim((string)$code, PHP_EOL) !== '0' + ? $templateSanitizer->sanitize(trim((string)$code, PHP_EOL)) + : null; + $result['markup'] = array_get($formData, 'markup') + ? $templateSanitizer->sanitize((string)array_get($formData, 'markup')) + : null; $settings = array_get($formData, 'settings', []);