diff --git a/app/Services/Pdf/PdfBuilder.php b/app/Services/Pdf/PdfBuilder.php index 987a6aaeb1..fb36dcabb1 100644 --- a/app/Services/Pdf/PdfBuilder.php +++ b/app/Services/Pdf/PdfBuilder.php @@ -85,6 +85,7 @@ public function build(): self public function getCompiledHTML($final = false) { $this->cleanHtml(); + $html = $this->document->saveHTML(); return str_replace('%24', '$', $html); @@ -99,7 +100,7 @@ private function cleanHtml(): self $dangerous_elements = [ 'iframe', 'form', 'object', 'embed', 'applet', 'audio', 'video', - 'frame', 'frameset', 'base','svg' + 'frame', 'frameset', 'base', 'svg' ]; $dangerous_attributes = [ diff --git a/app/Services/Pdf/PdfDesigner.php b/app/Services/Pdf/PdfDesigner.php index 95be694efd..c74c6a625e 100644 --- a/app/Services/Pdf/PdfDesigner.php +++ b/app/Services/Pdf/PdfDesigner.php @@ -44,6 +44,12 @@ public function build(): self $this->template = file_get_contents(config('ninja.designs.base_path') . strtolower($this->service->config->design->name) . '.html'); } + // Remove NULL bytes + $this->template = str_replace("\0", '', $this->template); + + // Remove UTF-7 BOM + $this->template = preg_replace('/^\\+ADw-/', '', $this->template); + return $this; } diff --git a/app/Services/PdfMaker/Design.php b/app/Services/PdfMaker/Design.php index 1f62dd50e1..ef81ca4b54 100644 --- a/app/Services/PdfMaker/Design.php +++ b/app/Services/PdfMaker/Design.php @@ -95,16 +95,33 @@ public function __construct(string $design = null, array $options = []) public function html(): ?string { if ($this->design == 'custom.html') { - return $this->composeFromPartials( + $design = $this->composeFromPartials( $this->options['custom_partials'] ); + + // Remove NULL bytes + $design = str_replace("\0", '', $design); + // Remove UTF-7 BOM + $design = preg_replace('/^\\+ADw-/', '', $design); + + return $design; + } $path = $this->options['custom_path'] ?? config('ninja.designs.base_path'); - return file_get_contents( + $design = file_get_contents( $path . $this->design ); + + + // Remove NULL bytes + $design = str_replace("\0", '', $design); + // Remove UTF-7 BOM + $design = preg_replace('/^\\+ADw-/', '', $design); + + return $design; + } public function elements(array $context, string $type = 'product'): array diff --git a/app/Services/Template/TemplateService.php b/app/Services/Template/TemplateService.php index 6f659d5fdb..b881dfa8b3 100644 --- a/app/Services/Template/TemplateService.php +++ b/app/Services/Template/TemplateService.php @@ -406,11 +406,137 @@ public function parseVariables(): self */ public function save(): self { + $this->cleanHtml(); + $this->compiled_html = str_replace('%24', '$', $this->document->saveHTML()); return $this; } + private function cleanHtml(): self + { + if (!$this->document || !$this->document->documentElement) { + return $this; + } + + $dangerous_elements = [ + 'iframe', 'form', 'object', 'embed', + 'applet', 'audio', 'video', + 'frame', 'frameset', 'base', 'svg' + ]; + + $dangerous_attributes = [ + 'onabort', 'onblur', 'onchange', 'onclick', 'ondblclick', + 'onerror', 'onfocus', 'onkeydown', 'onkeypress', 'onkeyup', + 'onload', 'onmousedown', 'onmousemove', 'onmouseout', + 'onmouseover', 'onmouseup', 'onreset', 'onresize', + 'onselect', 'onsubmit', 'onunload' + ]; + + // Function to recursively check nodes + $removeNodes = function ($node) use (&$removeNodes, $dangerous_elements, $dangerous_attributes) { + if (!$node) { + return; + } + + // Store children in array first to avoid modification during iteration + $children = []; + if ($node->hasChildNodes()) { + foreach ($node->childNodes as $child) { + $children[] = $child; + } + } + + // Process each child + foreach ($children as $child) { + $removeNodes($child); + } + + // Only process element nodes + if ($node instanceof \DOMElement) { + // Remove dangerous elements + if (in_array(strtolower($node->tagName), $dangerous_elements)) { + if ($node->parentNode) { + $node->parentNode->removeChild($node); + } + return; + } + + // Remove dangerous attributes + $attributes_to_remove = []; + foreach ($node->attributes as $attr) { + $attr_name = strtolower($attr->name); + $attr_value = strtolower($attr->value); + + // Remove event handlers + if (in_array($attr_name, $dangerous_attributes) || strpos($attr_name, 'on') === 0) { + $attributes_to_remove[] = $attr->name; + continue; + } + + // Remove dangerous URLs/protocols + if (in_array($attr_name, ['data', 'href', 'meta', 'link'])) { + if (preg_match('/(javascript|data|file|ftp|jar|dict|gopher|ldap|smb|php|alert|prompt|confirm):|\/\/\/\/+|127\.0\.0\.1|localhost/i', $attr_value)) { + $attributes_to_remove[] = $attr->name; + continue; + } + }else if ($attr_name === 'src') { + // For src attributes, only block dangerous protocols but allow data:image + if (preg_match('/(javascript|file|ftp|jar|dict|gopher|ldap|smb|php):|\/\/\/\/+|127\.0\.0\.1|localhost/i', $attr_value)) { + $attributes_to_remove[] = $attr->name; + continue; + } + // Additional check for data: URLs - only allow image types + if (strpos($attr_value, 'data:') === 0 && !preg_match('/^data:image\//i', $attr_value)) { + $attributes_to_remove[] = $attr->name; + continue; + } + + // Check for localhost references + if (preg_match('/localhost|127\.|0\.0\.0\.0|::1|0:0:0:0:0:0:0:1/i', $attr_value)) { + $attributes_to_remove[] = $attr->name; + continue; + } + + }elseif ($attr_name === 'style') { + + if (preg_match('/(expression|javascript|behavior|vbscript):|url\s*\(|import|@import|eval\s*\(|-moz-binding|behavior|expression/i', $attr_value)) { + $attributes_to_remove[] = $attr->name; + continue; + } + + } + + // Remove expressions + if (preg_match('/expression|javascript:|vbscript:|livescript:/i', $attr_value)) { + $attributes_to_remove[] = $attr->name; + continue; + } + } + + // Remove the collected dangerous attributes + foreach ($attributes_to_remove as $attr) { + $node->removeAttribute($attr); + } + } + }; + + try { + $removeNodes($this->document->documentElement); + } catch (\Exception $e) { + info('Error cleaning HTML: ' . $e->getMessage()); + + // Clear the document to prevent unsanitized content + $this->document = new \DOMDocument(); + + // Throw sanitized exception to alert calling code + throw new \RuntimeException('HTML sanitization failed'); + + } + + return $this; + } + /** * compose *