diff --git a/css/setup.scss b/css/setup.scss
index 327a5bc47..ee0e3b517 100644
--- a/css/setup.scss
+++ b/css/setup.scss
@@ -58,6 +58,9 @@ $progress-bar-error-bg-color: #F56565 !default;
.center {
text-align: center;
}
+.hidden {
+ display: none;
+}
/* Animations */
@keyframes progress_bar_color_ongoing {
diff --git a/setup/csp-detection.js b/setup/csp-detection.js
new file mode 100644
index 000000000..f10aaea0a
--- /dev/null
+++ b/setup/csp-detection.js
@@ -0,0 +1,121 @@
+/**
+ * Detects the current server's Content-Security-Policy to stop the setup if any directive doesn't meet the application's requirements
+ *
+ * @type {{_FindItopVersionInURI: (function(): string|string), aFlags: {bUnsafeInlineScriptOk: boolean, bUnsafeEvalScriptOk: boolean, bUnsafeInlineStyleOk: boolean}, _TestUnsafeEvalScript: SetupCSPDetection._TestUnsafeEvalScript, _HideContinueButtonIfPolicyNotOk: SetupCSPDetection._HideContinueButtonIfPolicyNotOk, _TestUnsafeInlineStyle: SetupCSPDetection._TestUnsafeInlineStyle, Run: SetupCSPDetection.Run, _TestUnSafeInlineScript: SetupCSPDetection._TestUnSafeInlineScript, _AddErrorAlert: SetupCSPDetection._AddErrorAlert}}
+ * @since 2.7.11 3.0.5 3.1.2 3.2.0 N°7075
+ */
+SetupCSPDetection = {
+ aFlags: {
+ bUnsafeInlineScriptOk: false,
+ bUnsafeEvalScriptOk: false,
+ bUnsafeInlineStyleOk: false,
+ },
+ Run: function () {
+ this._TestUnSafeInlineScript();
+ this._TestUnsafeEvalScript();
+ this._TestUnsafeInlineStyle();
+ this._HideContinueButtonIfPolicyNotOk();
+ },
+ /**
+ * Test if the CSP "unsafe-inline" directive for script-src if enabled, otherwise it forbids the setup to go further
+ * @private
+ */
+ _TestUnSafeInlineScript: function() {
+ var sBaitElemID = "csp-detection--unsafe-inline-script-bait";
+
+ // Add inline script that should add an element in the DOM
+ var sAddedScript = '';
+ $("body").append(sAddedScript);
+
+ // Check if element has been added to the DOM
+ if ($("#" + sBaitElemID).length === 1) {
+ this.aFlags.bUnsafeInlineScriptOk = true;
+ } else {
+ this._AddErrorAlert("unsafe-inline", "script");
+ }
+ },
+ /**
+ * Test if the CSP "unsafe-eval" directive for script-src if enabled, otherwise it forbids the setup to go further
+ * @private
+ */
+ _TestUnsafeEvalScript: function() {
+ var sBaitElemID = "csp-detection--unsafe-eval-script-bait";
+
+ // Add inline eval script that should add an element in the DOM
+ var sAddedScript = '';
+ $("body").append(sAddedScript);
+
+ // Check if element has been added to the DOM
+ if ($("#" + sBaitElemID).length === 1) {
+ this.aFlags.bUnsafeEvalScriptOk = true;
+ } else {
+ this._AddErrorAlert("unsafe-eval", "script");
+ }
+ },
+ /**
+ * Test if the CSP "unsafe-inline" directive for style-src if enabled, otherwise it forbids the setup to go further
+ * @private
+ */
+ _TestUnsafeInlineStyle: function() {
+ var sBaitElemID = "csp-detection--unsafe-inline-style-bait";
+
+ // Add inline eval script that should add an element in the DOM
+ $("body").append("
If this is present in the DOM and visible, then unsafe-inline for styles policy must be allowed
");
+ $("body").append("");
+
+ // Check if style has been applied
+ if ($("#" + sBaitElemID).is(":visible") === false) {
+ this.aFlags.bUnsafeInlineStyleOk = true;
+ } else {
+ // Remove bait div to avoid polluting the screen
+ $("#" + sBaitElemID).remove();
+ this._AddErrorAlert("unsafe-inline", "style");
+ }
+ },
+ /**
+ * Hide continue button to prevent setup from going further if any policy is not OK
+ * @private
+ */
+ _HideContinueButtonIfPolicyNotOk: function() {
+ if (false === this.aFlags.bUnsafeInlineScriptOk || false === this.aFlags.bUnsafeEvalScriptOk || false === this.aFlags.bUnsafeInlineStyleOk) {
+ // Hide next button to prevent user from going forward.
+ // Note that we don't remove it completely to be able to by-pass it.
+ $("#btn_next").addClass("ibo-is-hidden");
+ }
+ },
+ /**
+ * Internal helper to add an error alert in case of failure
+ * @param {String} sPolicyOption e.g. "unsafe-inline", "unsafe-eval", ...
+ * @param {String} sResourceType script|style
+ * @private
+ */
+ _AddErrorAlert: function(sPolicyOption, sResourceType) {
+ var sFilesType = sResourceType === "script" ? "scripts" : "styles";
+
+ // Add alert in the DOM
+ $("Error:Your server Content-Security-Policy header doesn't allow
" + sPolicyOption + " " + sFilesType + ". Therefore, the application cannot be installed (
see documentation).
")
+ .insertAfter(
+ $("#wiz_form h1:first")
+ );
+
+ },
+ /**
+ * Internal helper to find the iTop version as wiki syntax from the script URI
+ * @returns {string|string}
+ * @private
+ */
+ _FindItopVersionInURI: function() {
+ // First find script tag for the current file
+ var sScriptURI = $('script[src*="setup/csp-detection.js"]').attr("src");
+
+ // Extract parameter value from URI
+ var regex = new RegExp('[\\?&]' + 'itop_version_wiki_syntax' + '=([^]*)');
+ var results = regex.exec(sScriptURI);
+ return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
+ }
+};
+
+$(document).ready(function() {
+ SetupCSPDetection.Run();
+});
diff --git a/setup/setuppage.class.inc.php b/setup/setuppage.class.inc.php
index 1a145098a..35d10bf21 100644
--- a/setup/setuppage.class.inc.php
+++ b/setup/setuppage.class.inc.php
@@ -43,6 +43,7 @@ class SetupPage extends NiceWebPage
$this->LinkScriptFromAppRoot('node_modules/@popperjs/core/dist/umd/popper.js');
$this->LinkScriptFromAppRoot('node_modules/tippy.js/dist/tippy-bundle.umd.js');
$this->LinkScriptFromAppRoot("setup/setup.js");
+ $this->LinkScriptFromAppRoot("setup/csp-detection.js?itop_version_wiki_syntax=" . utils::GetItopVersionWikiSyntax());
$this->LinkStylesheetFromAppRoot('css/font-awesome/css/all.min.css');
$this->LinkStylesheetFromAppRoot('css/font-combodo/font-combodo.css');
$this->LinkStylesheetFromAppRoot('node_modules/tippy.js/dist/tippy.css');
diff --git a/sources/Application/WebPage/WebPage.php b/sources/Application/WebPage/WebPage.php
index 424aa8264..b731e51d6 100644
--- a/sources/Application/WebPage/WebPage.php
+++ b/sources/Application/WebPage/WebPage.php
@@ -544,7 +544,9 @@ class WebPage implements Page
return;
}
- if (false === utils::RealPath(APPROOT . $sFileRelPath, APPROOT)) {
+ // Ensure file is within the app folder
+ $sFileRelPathWithoutQueryParams = explode("?", $sFileRelPath)[0];
+ if (false === utils::RealPath(APPROOT . $sFileRelPathWithoutQueryParams, APPROOT)) {
IssueLog::Warning("Linked resource added to page with a path from outside app directory, it will be ignored.", LogChannels::CONSOLE, [
"linked_resource_uri" => $sFileRelPath,
"request_uri" => $_SERVER['REQUEST_URI'] ?? '' /* CLI */,
@@ -580,7 +582,9 @@ class WebPage implements Page
return;
}
- $sFileAbsPath = MODULESROOT . $sFileRelPath;
+ // Ensure file is within the app folder
+ $sFileRelPathWithoutQueryParams = explode("?", $sFileRelPath)[0];
+ $sFileAbsPath = MODULESROOT . $sFileRelPathWithoutQueryParams;
// For modules only, we don't check real path if symlink as the file would not be in under MODULESROOT
if (false === is_link($sFileAbsPath) && false === utils::RealPath($sFileAbsPath, MODULESROOT)) {
IssueLog::Warning("Linked resource added to page with a path from outside current env. directory, it will be ignored.", LogChannels::CONSOLE, [
diff --git a/tests/php-unit-tests/unitary-tests/sources/Application/WebPage/WebPageTest.php b/tests/php-unit-tests/unitary-tests/sources/Application/WebPage/WebPageTest.php
index ec802bcc6..82bc6f601 100644
--- a/tests/php-unit-tests/unitary-tests/sources/Application/WebPage/WebPageTest.php
+++ b/tests/php-unit-tests/unitary-tests/sources/Application/WebPage/WebPageTest.php
@@ -58,6 +58,11 @@ class WebPageTest extends ItopDataTestCase
"js/utils.js",
1,
],
+ "LinkScriptFromAppRoot: Relative URI of existing file with query params should be completed / added" => [
+ "LinkScriptFromAppRoot",
+ "js/utils.js?foo=bar",
+ 1,
+ ],
"LinkScriptFromAppRoot: Relative URI of NON existing file should be ignored" => [
"LinkScriptFromAppRoot",
"js/some-file.js",
@@ -80,6 +85,11 @@ class WebPageTest extends ItopDataTestCase
"itop-portal-base/portal/public/js/toolbox.js",
1,
],
+ "LinkScriptFromModule: Relative URI of existing file with query params should be completed / added" => [
+ "LinkScriptFromModule",
+ "itop-portal-base/portal/public/js/toolbox.js?foo=bar",
+ 1,
+ ],
"LinkScriptFromModule: Relative URI of NON existing file should be completed / added" => [
"LinkScriptFromModule",
"some-module/asset/js/some-file.js",
@@ -107,6 +117,11 @@ class WebPageTest extends ItopDataTestCase
"https://external.server/file.js",
1,
],
+ "LinkScriptFromURI: Absolute URI with query params should be added" => [
+ "LinkScriptFromURI",
+ "https://external.server/file.js?foo=bar",
+ 1,
+ ],
];
}
@@ -148,6 +163,11 @@ class WebPageTest extends ItopDataTestCase
"css/login.css",
1,
],
+ "LinkStylesheetFromAppRoot: Relative URI of existing file with query params should be completed / added" => [
+ "LinkStylesheetFromAppRoot",
+ "css/login.css?foo=bar",
+ 1,
+ ],
"LinkStylesheetFromAppRoot: Relative URI of NON existing file should be ignored" => [
"LinkStylesheetFromAppRoot",
"css/some-file.css",
@@ -170,6 +190,11 @@ class WebPageTest extends ItopDataTestCase
"itop-portal-base/portal/public/css/portal.css",
1,
],
+ "LinkStylesheetFromModule: Relative URI of existing file with query params should be completed / added" => [
+ "LinkStylesheetFromModule",
+ "itop-portal-base/portal/public/css/portal.css?foo=bar",
+ 1,
+ ],
"LinkStylesheetFromModule: Relative URI of NON existing file should be completed / added" => [
"LinkStylesheetFromModule",
"some-module/asset/js/some-file.js",
@@ -197,6 +222,11 @@ class WebPageTest extends ItopDataTestCase
"https://external.server/file.css",
1,
],
+ "LinkStylesheetFromURI: Absolute URI with query params should be added" => [
+ "LinkStylesheetFromURI",
+ "https://external.server/file.css?foo=bar",
+ 1,
+ ],
];
}
}
\ No newline at end of file