From ca9532b6e98259299a5d1d7564c724d75ee0a01e Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Thu, 6 Nov 2025 14:17:36 +0100 Subject: [PATCH] fix: check arg length (external liquid renderer) --- app/Models/Plugin.php | 10 +++-- app/Services/PluginImportService.php | 55 ++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index 40569f8..ab83514 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -11,6 +11,7 @@ use App\Liquid\Filters\StandardFilters; use App\Liquid\Filters\StringMarkup; use App\Liquid\Filters\Uniqueness; use App\Liquid\Tags\TemplateTag; +use App\Services\PluginImportService; use Exception; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -372,14 +373,14 @@ class Plugin extends Model * @param array $context The render context data * @return string The rendered HTML * - * @throws LiquidException + * @throws Exception */ private function renderWithExternalLiquidRenderer(string $template, array $context): string { $liquidPath = config('services.trmnl.liquid_path'); if (empty($liquidPath)) { - throw new LiquidException('External liquid renderer path is not configured'); + throw new Exception('External liquid renderer path is not configured'); } // HTML encode the template @@ -389,9 +390,12 @@ class Plugin extends Model $jsonContext = json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($jsonContext === false) { - throw new LiquidException('Failed to encode render context as JSON: '.json_last_error_msg()); + throw new Exception('Failed to encode render context as JSON: '.json_last_error_msg()); } + // Validate argument sizes + app(PluginImportService::class)->validateExternalRendererArguments($encodedTemplate, $jsonContext, $liquidPath); + // Execute the external renderer $process = Process::run([ $liquidPath, diff --git a/app/Services/PluginImportService.php b/app/Services/PluginImportService.php index 7c77767..a245f65 100644 --- a/app/Services/PluginImportService.php +++ b/app/Services/PluginImportService.php @@ -381,4 +381,59 @@ class PluginImportService 'sharedLiquidPath' => $sharedLiquidPath, ]; } + + /** + * Validate that template and context are within command-line argument limits + * + * @param string $template The liquid template string + * @param string $jsonContext The JSON-encoded context + * @param string $liquidPath The path to the liquid renderer executable + * @return void + * + * @throws Exception If the template or context exceeds argument limits + */ + public function validateExternalRendererArguments(string $template, string $jsonContext, string $liquidPath): void + { + // MAX_ARG_STRLEN on Linux is typically 131072 (128KB) for individual arguments + // ARG_MAX is the total size of all arguments (typically 2MB on modern systems) + $maxIndividualArgLength = 131072; // 128KB - MAX_ARG_STRLEN limit + $maxTotalArgLength = $this->getMaxArgumentLength(); + + // Check individual argument sizes (template and context are the largest) + if (strlen($template) > $maxIndividualArgLength || strlen($jsonContext) > $maxIndividualArgLength) { + throw new Exception('Context too large for external liquid renderer. Reduce the size of the Payload or Template.'); + } + + // Calculate total size of all arguments (path + flags + template + context) + // Add overhead for path, flags, and separators (conservative estimate: ~200 bytes) + $totalArgSize = strlen($liquidPath) + strlen('--template') + strlen($template) + + strlen('--context') + strlen($jsonContext) + 200; + + if ($totalArgSize > $maxTotalArgLength) { + throw new Exception('Context too large for external liquid renderer. Reduce the size of the Payload or Template.'); + } + } + + /** + * Get the maximum argument length for command-line arguments + * + * @return int Maximum argument length in bytes + */ + private function getMaxArgumentLength(): int + { + // Try to get ARG_MAX from system using getconf + $argMax = null; + if (function_exists('shell_exec')) { + $result = @shell_exec('getconf ARG_MAX 2>/dev/null'); + if ($result !== null && is_numeric(trim($result))) { + $argMax = (int) trim($result); + } + } + + // Use conservative fallback if ARG_MAX cannot be determined + // ARG_MAX on macOS is typically 262144 (256KB), on Linux it's usually 2097152 (2MB) + // We use 200KB as a conservative limit that works on both systems + // Note: ARG_MAX includes environment variables, so we leave headroom + return $argMax !== null ? min($argMax, 204800) : 204800; + } }