diff --git a/src/Controller/AttachmentFileController.php b/src/Controller/AttachmentFileController.php index 81369e12..c16c1e85 100644 --- a/src/Controller/AttachmentFileController.php +++ b/src/Controller/AttachmentFileController.php @@ -30,6 +30,7 @@ use App\Form\Filters\AttachmentFilterType; use App\Services\Attachments\AttachmentManager; use App\Services\Trees\NodesListBuilder; use App\Settings\BehaviorSettings\TableSettings; +use App\Settings\SystemSettings\AttachmentsSettings; use Omines\DataTablesBundle\DataTableFactory; use RuntimeException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -41,27 +42,50 @@ use Symfony\Component\Routing\Attribute\Route; class AttachmentFileController extends AbstractController { + + public function __construct(private readonly AttachmentManager $helper) + { + + } + + #[Route(path: '/attachment/{id}/sandbox', name: 'attachment_html_sandbox')] + public function htmlSandbox(Attachment $attachment, AttachmentsSettings $attachmentsSettings): Response + { + //Check if the sandbox is enabled in the settings, as it can be a security risk if used without proper precautions, so it should be opt-in + if (!$attachmentsSettings->showHTMLAttachments) { + throw $this->createAccessDeniedException('The HTML sandbox for attachments is disabled in the settings, as it can be a security risk if used without proper precautions. Please enable it in the settings if you want to use it.'); + } + + $this->checkPermissions($attachment); + + $file_path = $this->helper->toAbsoluteInternalFilePath($attachment); + + $attachmentContent = file_get_contents($file_path); + + $response = $this->render('attachments/html_sandbox.html.twig', [ + 'attachment' => $attachment, + 'content' => $attachmentContent, + ]); + + //Set an CSP that allows to run inline scripts, styles and images from external ressources, but does not allow any connections or others. + //Also set the sandbox CSP directive with only "allow-script" to run basic scripts + $response->headers->set('Content-Security-Policy', "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; sandbox allow-scripts;"); + + //Forbid to embed the attachment render page in an iframe to prevent clickjacking, as it is not used anywhere else for now + $response->headers->set('X-Frame-Options', 'DENY'); + + return $response; + } + /** * Download the selected attachment. */ #[Route(path: '/attachment/{id}/download', name: 'attachment_download')] - public function download(Attachment $attachment, AttachmentManager $helper): BinaryFileResponse + public function download(Attachment $attachment): BinaryFileResponse { - $this->denyAccessUnlessGranted('read', $attachment); + $this->checkPermissions($attachment); - if ($attachment->isSecure()) { - $this->denyAccessUnlessGranted('show_private', $attachment); - } - - if (!$attachment->hasInternal()) { - throw $this->createNotFoundException('The file for this attachment is external and not stored locally!'); - } - - if (!$helper->isInternalFileExisting($attachment)) { - throw $this->createNotFoundException('The file associated with the attachment is not existing!'); - } - - $file_path = $helper->toAbsoluteInternalFilePath($attachment); + $file_path = $this->helper->toAbsoluteInternalFilePath($attachment); $response = new BinaryFileResponse($file_path); //Set header content disposition, so that the file will be downloaded @@ -74,7 +98,20 @@ class AttachmentFileController extends AbstractController * View the attachment. */ #[Route(path: '/attachment/{id}/view', name: 'attachment_view')] - public function view(Attachment $attachment, AttachmentManager $helper): BinaryFileResponse + public function view(Attachment $attachment): BinaryFileResponse + { + $this->checkPermissions($attachment); + + $file_path = $this->helper->toAbsoluteInternalFilePath($attachment); + $response = new BinaryFileResponse($file_path); + + //Set header content disposition, so that the file will be downloaded + $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE); + + return $response; + } + + private function checkPermissions(Attachment $attachment): void { $this->denyAccessUnlessGranted('read', $attachment); @@ -86,17 +123,9 @@ class AttachmentFileController extends AbstractController throw $this->createNotFoundException('The file for this attachment is external and not stored locally!'); } - if (!$helper->isInternalFileExisting($attachment)) { + if (!$this->helper->isInternalFileExisting($attachment)) { throw $this->createNotFoundException('The file associated with the attachment is not existing!'); } - - $file_path = $helper->toAbsoluteInternalFilePath($attachment); - $response = new BinaryFileResponse($file_path); - - //Set header content disposition, so that the file will be downloaded - $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE); - - return $response; } #[Route(path: '/attachment/list', name: 'attachment_list')] diff --git a/src/Settings/SystemSettings/AttachmentsSettings.php b/src/Settings/SystemSettings/AttachmentsSettings.php index 6d15c639..2a682b11 100644 --- a/src/Settings/SystemSettings/AttachmentsSettings.php +++ b/src/Settings/SystemSettings/AttachmentsSettings.php @@ -58,4 +58,11 @@ class AttachmentsSettings envVar: "bool:ATTACHMENT_DOWNLOAD_BY_DEFAULT", envVarMode: EnvVarMode::OVERWRITE )] public bool $downloadByDefault = false; -} \ No newline at end of file + + #[SettingsParameter( + label: new TM("settings.system.attachments.showHTMLAttachments"), + description: new TM("settings.system.attachments.showHTMLAttachments.help"), + envVar: "bool:ATTACHMENT_SHOW_HTML", envVarMode: EnvVarMode::OVERWRITE + )] + public bool $showHTMLAttachments = false; +} diff --git a/templates/attachments/html_sandbox.html.twig b/templates/attachments/html_sandbox.html.twig new file mode 100644 index 00000000..6706da7d --- /dev/null +++ b/templates/attachments/html_sandbox.html.twig @@ -0,0 +1,75 @@ + + + + + + {# The content block is already escaped. so we must not escape it again. #} +