mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-02-25 19:09:49 +00:00
Added basic functionality for an HTML sandbox for relative safely rendering HTML attachments
Fixed #1150
This commit is contained in:
parent
a7a1026f9b
commit
63dd344c02
4 changed files with 161 additions and 26 deletions
|
|
@ -30,6 +30,7 @@ use App\Form\Filters\AttachmentFilterType;
|
||||||
use App\Services\Attachments\AttachmentManager;
|
use App\Services\Attachments\AttachmentManager;
|
||||||
use App\Services\Trees\NodesListBuilder;
|
use App\Services\Trees\NodesListBuilder;
|
||||||
use App\Settings\BehaviorSettings\TableSettings;
|
use App\Settings\BehaviorSettings\TableSettings;
|
||||||
|
use App\Settings\SystemSettings\AttachmentsSettings;
|
||||||
use Omines\DataTablesBundle\DataTableFactory;
|
use Omines\DataTablesBundle\DataTableFactory;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
|
@ -41,27 +42,50 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
class AttachmentFileController extends AbstractController
|
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.
|
* Download the selected attachment.
|
||||||
*/
|
*/
|
||||||
#[Route(path: '/attachment/{id}/download', name: 'attachment_download')]
|
#[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()) {
|
$file_path = $this->helper->toAbsoluteInternalFilePath($attachment);
|
||||||
$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);
|
|
||||||
$response = new BinaryFileResponse($file_path);
|
$response = new BinaryFileResponse($file_path);
|
||||||
|
|
||||||
//Set header content disposition, so that the file will be downloaded
|
//Set header content disposition, so that the file will be downloaded
|
||||||
|
|
@ -74,7 +98,20 @@ class AttachmentFileController extends AbstractController
|
||||||
* View the attachment.
|
* View the attachment.
|
||||||
*/
|
*/
|
||||||
#[Route(path: '/attachment/{id}/view', name: 'attachment_view')]
|
#[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);
|
$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!');
|
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!');
|
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')]
|
#[Route(path: '/attachment/list', name: 'attachment_list')]
|
||||||
|
|
|
||||||
|
|
@ -58,4 +58,11 @@ class AttachmentsSettings
|
||||||
envVar: "bool:ATTACHMENT_DOWNLOAD_BY_DEFAULT", envVarMode: EnvVarMode::OVERWRITE
|
envVar: "bool:ATTACHMENT_DOWNLOAD_BY_DEFAULT", envVarMode: EnvVarMode::OVERWRITE
|
||||||
)]
|
)]
|
||||||
public bool $downloadByDefault = false;
|
public bool $downloadByDefault = false;
|
||||||
}
|
|
||||||
|
#[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;
|
||||||
|
}
|
||||||
|
|
|
||||||
75
templates/attachments/html_sandbox.html.twig
Normal file
75
templates/attachments/html_sandbox.html.twig
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ app.request.locale | replace({"_": "-"}) }}"
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
|
||||||
|
{# The content block is already escaped. so we must not escape it again. #}
|
||||||
|
<title></title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Reset margins and stop the page from scrolling */
|
||||||
|
body, html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The Flex Container */
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The Warning Header */
|
||||||
|
.warning-bar {
|
||||||
|
background-color: #ff4d4d;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||||
|
z-index: 10; /* Keep it above the iframe */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The Iframe: The 'flex: 1' makes it fill all remaining space */
|
||||||
|
.content-frame {
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block body %}
|
||||||
|
{# We have a fullscreen iframe, with an warning on top #}
|
||||||
|
|
||||||
|
<div class="wrapper">
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<header class="warning-bar">
|
||||||
|
<b>⚠️ {% trans%}attachment.sandbox.warning{% endtrans %}</b>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<small>
|
||||||
|
{% trans%}[Attachment]{% endtrans%}: {{ attachment.name }} / {{ attachment.filename ?? "" }} ({% trans%}id.label{% endtrans %}: {{ attachment.id }})
|
||||||
|
<a href="{{ path("homepage") }}" style="color: white; margin-left: 15px;">{% trans%}attachment.sandbox.back_to_partdb{% endtrans %}</a>
|
||||||
|
</small>
|
||||||
|
</header>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<iframe referrerpolicy="no-referrer" class="content-frame"
|
||||||
|
{# When changing this sandbox, also change the sandbox CSP in the controller #}
|
||||||
|
sandbox="allow-scripts"
|
||||||
|
srcdoc="{{ content|e('html_attr') }}"
|
||||||
|
></iframe>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -12593,5 +12593,29 @@ Buerklin-API Authentication server:
|
||||||
<target>When selected, more details will be fetched from canopy when creating a part. This causes an additional API request, but gives product bullet points and category info.</target>
|
<target>When selected, more details will be fetched from canopy when creating a part. This causes an additional API request, but gives product bullet points and category info.</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
|
<unit id="D055xh8" name="attachment.sandbox.warning">
|
||||||
|
<segment>
|
||||||
|
<source>attachment.sandbox.warning</source>
|
||||||
|
<target>WARNING: You are viewing an user uploaded attachment. This is untrusted content. Proceed with care.</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bRcdnJK" name="attachment.sandbox.back_to_partdb">
|
||||||
|
<segment>
|
||||||
|
<source>attachment.sandbox.back_to_partdb</source>
|
||||||
|
<target>Back to Part-DB</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="MzyA7N8" name="settings.system.attachments.showHTMLAttachments">
|
||||||
|
<segment>
|
||||||
|
<source>settings.system.attachments.showHTMLAttachments</source>
|
||||||
|
<target>Show uploaded HTML file attachments (sandboxed)</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="V_LJkRy" name="settings.system.attachments.showHTMLAttachments.help">
|
||||||
|
<segment>
|
||||||
|
<source>settings.system.attachments.showHTMLAttachments.help</source>
|
||||||
|
<target>⚠️ When enabled, user uploaded HTML attachments can be viewed directly in the browser. Many potential malicious functions are restricted, still this is a potential security risk and should only be enabled, if you trust the users who can upload files.</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue