From 95ce8645f2461042103b6f3ce0c0dc4fade2299b Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Tue, 9 Aug 2022 14:30:01 +0300 Subject: [PATCH] client headers --- .../Espo/Core/Utils/Client/ActionRenderer.php | 14 ++++ .../Utils/Client/ActionRenderer/Params.php | 66 +++++++++++++++++++ application/Espo/Core/Utils/ClientManager.php | 65 +++++++++++++++++- .../Espo/EntryPoints/ChangePassword.php | 4 +- application/Espo/EntryPoints/ConfirmOptIn.php | 24 +++---- .../Espo/Resources/defaults/config.php | 6 ++ .../Espo/Resources/defaults/systemConfig.php | 3 + html/main.html | 2 +- 8 files changed, 164 insertions(+), 20 deletions(-) create mode 100644 application/Espo/Core/Utils/Client/ActionRenderer/Params.php diff --git a/application/Espo/Core/Utils/Client/ActionRenderer.php b/application/Espo/Core/Utils/Client/ActionRenderer.php index 27a2aa3cf7..ea05821f8b 100644 --- a/application/Espo/Core/Utils/Client/ActionRenderer.php +++ b/application/Espo/Core/Utils/Client/ActionRenderer.php @@ -29,6 +29,8 @@ namespace Espo\Core\Utils\Client; +use Espo\Core\Api\Response; +use Espo\Core\Utils\Client\ActionRenderer\Params; use Espo\Core\Utils\Json; use Espo\Core\Utils\ClientManager; @@ -45,6 +47,18 @@ class ActionRenderer } /** + * Writes to a body. + */ + public function write(Response $response, Params $params): void + { + $body = $this->render($params->getController(), $params->getAction(), $params->getData()); + + $this->clientManager->writeHeaders($response); + $response->writeBody($body); + } + + /** + * @deprecated Use`write`. * @param ?array $data */ public function render(string $controller, string $action, ?array $data = null): string diff --git a/application/Espo/Core/Utils/Client/ActionRenderer/Params.php b/application/Espo/Core/Utils/Client/ActionRenderer/Params.php new file mode 100644 index 0000000000..7eac94a2f5 --- /dev/null +++ b/application/Espo/Core/Utils/Client/ActionRenderer/Params.php @@ -0,0 +1,66 @@ + */ + private ?array $data; + + /** + * @param ?array $data + */ + public function __construct(string $controller, string $action, ?array $data = null) + { + $this->controller = $controller; + $this->action = $action; + $this->data = $data; + } + + public function getController(): string + { + return $this->controller; + } + + public function getAction(): string + { + return $this->action; + } + + /** + * @return ?array + */ + public function getData(): ?array + { + return $this->data; + } +} diff --git a/application/Espo/Core/Utils/ClientManager.php b/application/Espo/Core/Utils/ClientManager.php index 9bfc231753..1d195577a9 100644 --- a/application/Espo/Core/Utils/ClientManager.php +++ b/application/Espo/Core/Utils/ClientManager.php @@ -30,12 +30,17 @@ namespace Espo\Core\Utils; use Espo\Core\{ + Api\Response, + Api\ResponseWrapper, Utils\File\Manager as FileManager, Utils\Client\DevModeJsFileListProvider, Utils\Module, Utils\Json, }; +use Slim\Psr7\Response as Psr7Response; +use Slim\ResponseEmitter; + /** * Renders the main HTML page. */ @@ -61,6 +66,8 @@ class ClientManager private Module $module; + private string $nonce; + private const APP_DESCRIPTION = "EspoCRM - Open Source CRM application."; public function __construct( @@ -77,6 +84,8 @@ class ClientManager $this->fileManager = $fileManager; $this->devModeJsFileListProvider = $devModeJsFileListProvider; $this->module = $module; + + $this->nonce = Util::generateKey(); } public function setBasePath(string $basePath): void @@ -98,12 +107,65 @@ class ClientManager return $this->config->get('cacheTimestamp', 0); } + public function writeHeaders(Response $response): void + { + $this->writeContentSecurityPolicyHeader($response); + $this->writeStrictTransportSecurityHeader($response); + + /** @var array $headers */ + $headers = $this->config->get('clientHttpHeaders') ?? []; + + foreach ($headers as $name => $value) { + if ($value === null) { + continue; + } + + $response->setHeader($name, $value); + } + } + + /** + * @todo Move to a separate class. + */ + private function writeContentSecurityPolicyHeader(Response $response): void + { + if ($this->config->get('clientCspDisabled')) { + return; + } + + $scriptSrc = "script-src 'self' 'nonce-{$this->nonce}' 'unsafe-eval'"; + + $scriptSourceList = $this->config->get('clientCspScriptSourceList') ?? []; + + foreach ($scriptSourceList as $src) { + $scriptSrc .= ' ' . $src; + } + + $response->setHeader('Content-Security-Policy', $scriptSrc); + } + + private function writeStrictTransportSecurityHeader(Response $response) + { + $siteUrl =$this->config->get('siteUrl') ?? ''; + + if (strpos($siteUrl, 'https://') === 0) { + $response->setHeader('Strict-Transport-Security', 'max-age=10368000'); + } + } + /** * @param array $vars */ public function display(?string $runScript = null, ?string $htmlFilePath = null, array $vars = []): void { - echo $this->render($runScript, $htmlFilePath, $vars); + $body = $this->render($runScript, $htmlFilePath, $vars); + + $response = new ResponseWrapper(new Psr7Response()); + + $this->writeHeaders($response); + $response->writeBody($body); + + (new ResponseEmitter())->emit($response->getResponse()); } /** @@ -205,6 +267,7 @@ class ClientManager 'libsConfigPath' => $this->libsConfigPath, 'internalModuleList' => Json::encode($internalModuleList), 'applicationDescription' => $this->config->get('applicationDescription') ?? self::APP_DESCRIPTION, + 'nonce' => $this->nonce, ]; $html = $this->fileManager->getContents($htmlFilePath); diff --git a/application/Espo/EntryPoints/ChangePassword.php b/application/Espo/EntryPoints/ChangePassword.php index ce4caaf020..b18e5249f8 100644 --- a/application/Espo/EntryPoints/ChangePassword.php +++ b/application/Espo/EntryPoints/ChangePassword.php @@ -92,8 +92,8 @@ class ChangePassword implements EntryPoint 'notFound' => !$passwordChangeRequest, ]; - $html = $this->actionRenderer->render('controllers/password-change-request', 'passwordChange', $options); + $params = new ActionRenderer\Params('controllers/password-change-request', 'passwordChange', $options); - $response->writeBody($html); + $this->actionRenderer->write($response, $params); } } diff --git a/application/Espo/EntryPoints/ConfirmOptIn.php b/application/Espo/EntryPoints/ConfirmOptIn.php index 799e36db07..bd94b5dba3 100644 --- a/application/Espo/EntryPoints/ConfirmOptIn.php +++ b/application/Espo/EntryPoints/ConfirmOptIn.php @@ -36,23 +36,21 @@ use Espo\Core\{ Exceptions\Error, EntryPoint\EntryPoint, EntryPoint\Traits\NoAuth, - Utils\ClientManager, + Utils\Client\ActionRenderer, Api\Request, - Api\Response, -}; + Api\Response}; class ConfirmOptIn implements EntryPoint { use NoAuth; - private $clientManager; + private Service $service; + private ActionRenderer $actionRenderer; - private $service; - - public function __construct(ClientManager $clientManager, Service $service) + public function __construct(Service $service, ActionRenderer $actionRenderer) { - $this->clientManager = $clientManager; $this->service = $service; + $this->actionRenderer = $actionRenderer; } /** @@ -76,14 +74,8 @@ class ConfirmOptIn implements EntryPoint $action = 'optInConfirmationSuccess'; } - $runScript = " - require('controllers/lead-capture-opt-in-confirmation', Controller => { - var controller = new Controller(app.baseController.params, app.getControllerInjection()); - controller.masterView = app.masterView; - controller.doAction('{$action}', " . json_encode($data) . "); - }); - "; + $params = new ActionRenderer\Params('controllers/lead-capture-opt-in-confirmation', $action, $data); - $this->clientManager->display($runScript); + $this->actionRenderer->write($response, $params); } } diff --git a/application/Espo/Resources/defaults/config.php b/application/Espo/Resources/defaults/config.php index 21e774a442..e58218c681 100644 --- a/application/Espo/Resources/defaults/config.php +++ b/application/Espo/Resources/defaults/config.php @@ -215,5 +215,11 @@ return [ 'passwordGenerateLength' => 10, 'massActionIdleCountThreshold' => 100, 'exportIdleCountThreshold' => 1000, + 'clientHttpHeaders' => [ + 'X-Frame-Options' => 'SAMEORIGIN', + 'X-Content-Type-Options' => 'nosniff', + ], + 'clientCspDisabled' => false, + 'clientCspScriptSourceList' => [], 'isInstalled' => false, ]; diff --git a/application/Espo/Resources/defaults/systemConfig.php b/application/Espo/Resources/defaults/systemConfig.php index e70eb1ebd7..7c4c3ef16e 100644 --- a/application/Espo/Resources/defaults/systemConfig.php +++ b/application/Espo/Resources/defaults/systemConfig.php @@ -98,6 +98,9 @@ return [ 'webSocketMessager', 'actualDatabaseType', 'actualDatabaseVersion', + 'clientHttpHeaders', + 'clientCspDisabled', + 'clientCspScriptSourceList', ], 'adminItems' => [ 'devMode', diff --git a/html/main.html b/html/main.html index 368707d3d5..4fd4a98b10 100644 --- a/html/main.html +++ b/html/main.html @@ -12,7 +12,7 @@ -