Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/Flame/Database/Attach/MediaAdder.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class MediaAdder

protected ?Model $performedOn = null;

protected string $tag = 'default';
protected ?string $tag = 'default';

protected ?string $diskName = null;

Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/Flame/Pagic/PagicServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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, [
Expand Down
149 changes: 149 additions & 0 deletions src/Flame/Pagic/TemplateSandbox.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

declare(strict_types=1);

namespace Igniter\Flame\Pagic;

class TemplateSandbox
{
public function sanitize(string $template): string
{
// Order matters - most dangerous first
$template = $this->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 <?xml)
$content = preg_replace('/<\?(?!xml)[\s\S]*$/i', '', $content) ?? $content;

// Catch any remnants
$content = str_replace(['<?', '?>'], '', $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;
}
}
2 changes: 1 addition & 1 deletion src/Flame/Support/Igniter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 8 additions & 2 deletions src/Main/FormWidgets/TemplateEditor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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', []);

Expand Down
Loading