From 98f946c871bce68c19929e257571124312715b48 Mon Sep 17 00:00:00 2001
From: jf-cbd
Date: Fri, 7 Jun 2024 14:18:14 +0200
Subject: [PATCH 1/2] =?UTF-8?q?=20N=C2=B07124=20-=20[SECU]=20Cross-Site=20?=
=?UTF-8?q?Request=20Forgery=20(CSRF)=20in=20several=20iTop=20pages=20(fin?=
=?UTF-8?q?alize=20implementation)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../portal/templates/layout.html.twig | 1 +
js/ajax_hook.js | 5 +++++
js/ckeditor.feeds.js | 6 +-----
js/ckeditor.handler.js | 7 ++-----
js/icon_select.js | 6 +-----
js/pages/backoffice/on-ready.js | 5 -----
js/utils.js | 17 +++++++++++++++++
pages/ajax.render.php | 4 ++--
sources/Application/WebPage/NiceWebPage.php | 1 +
.../Controller/OAuth/OAuthLandingController.php | 1 +
10 files changed, 31 insertions(+), 22 deletions(-)
create mode 100644 js/ajax_hook.js
diff --git a/datamodels/2.x/itop-portal-base/portal/templates/layout.html.twig b/datamodels/2.x/itop-portal-base/portal/templates/layout.html.twig
index 2b1780082..a584b168e 100644
--- a/datamodels/2.x/itop-portal-base/portal/templates/layout.html.twig
+++ b/datamodels/2.x/itop-portal-base/portal/templates/layout.html.twig
@@ -87,6 +87,7 @@
{% block pPageScripts %}
+
{% if is_development_environment() %}
{% else %}
diff --git a/js/ajax_hook.js b/js/ajax_hook.js
new file mode 100644
index 000000000..aa91d51e2
--- /dev/null
+++ b/js/ajax_hook.js
@@ -0,0 +1,5 @@
+// add X-Combodo-Ajax for all request (just after jaquery is loaded)
+// mandatory for ajax requests with JQuery (CSRF protection)
+$(document).ajaxSend(function (event, jqxhr, options) {
+ jqxhr.setRequestHeader('X-Combodo-Ajax', 'true');
+});
\ No newline at end of file
diff --git a/js/ckeditor.feeds.js b/js/ckeditor.feeds.js
index b06279602..0eed695d4 100644
--- a/js/ckeditor.feeds.js
+++ b/js/ckeditor.feeds.js
@@ -16,11 +16,7 @@ const CombodoCKEditorFeeds = {
return async function(queryText) {
return new Promise( resolve => {
setTimeout( () => {
- fetch(options.url + queryText, {
- headers: {
- 'X-Combodo-Ajax': true
- }
- })
+ CombodoHTTP.Fetch(options.url + queryText)
.then(response => {
return response.json();
})
diff --git a/js/ckeditor.handler.js b/js/ckeditor.handler.js
index 390cd29a5..7f7cfc836 100644
--- a/js/ckeditor.handler.js
+++ b/js/ckeditor.handler.js
@@ -112,12 +112,9 @@ const CombodoCKEditorHandler = {
const formData = new FormData();
formData.append('upload', file);
- fetch(uploadUrl, {
+ CombodoHTTP.Fetch(uploadUrl, {
method: 'POST',
- body: formData,
- headers: {
- 'X-Combodo-Ajax': true
- },
+ body: formData
})
.then(response => response.json())
.then(responseData => {
diff --git a/js/icon_select.js b/js/icon_select.js
index 5eaa590b7..a9e6bfcfb 100644
--- a/js/icon_select.js
+++ b/js/icon_select.js
@@ -247,12 +247,8 @@ $(function()
var file = event.target.files[0];
var formData = new FormData();
formData.append('file', file);
-
- fetch(this.options.post_upload_to, {
+ CombodoHTTP.Fetch(this.options.post_upload_to, {
method: 'POST',
- headers: {
- 'X-Combodo-Ajax': true
- },
body: formData
})
.then(response => {
diff --git a/js/pages/backoffice/on-ready.js b/js/pages/backoffice/on-ready.js
index e477de897..1d7f54192 100644
--- a/js/pages/backoffice/on-ready.js
+++ b/js/pages/backoffice/on-ready.js
@@ -8,11 +8,6 @@
*/
$(document).ready(function () {
- // AJAX calls handling
- // - Custom headers
- $(document).ajaxSend(function (event, jqxhr, options) {
- jqxhr.setRequestHeader('X-Combodo-Ajax', 'true');
- });
// - Error messages regarding the error code
$(document).ajaxError(function (event, jqxhr, options) {
// User is not logged in
diff --git a/js/utils.js b/js/utils.js
index 5bb9fc24f..4d51b4ee0 100644
--- a/js/utils.js
+++ b/js/utils.js
@@ -1296,6 +1296,23 @@ const CombodoInlineImage = {
}
};
+/**
+ * Abstract Fetch API wrapper to manage AJAX requests in iTop.
+ */
+const CombodoHTTP = {
+ /**
+ * @param {string} sUrl URL to fetch
+ * @param {Object} oOptions Fetch options
+ * @return {Promise}
+ */
+ Fetch: function(sUrl, oOptions) {
+ oOptions = oOptions || {};
+ oOptions.headers = oOptions.headers || {};
+ oOptions.headers['X-Combodo-Ajax'] = true;
+ return fetch(sUrl, oOptions);
+ }
+}
+
/**
* Abstract wrapper to manage modal dialogs in iTop.
* Implementations for the various GUIs may vary but APIs are the same.
diff --git a/pages/ajax.render.php b/pages/ajax.render.php
index 4a194a869..89135c9db 100644
--- a/pages/ajax.render.php
+++ b/pages/ajax.render.php
@@ -29,12 +29,12 @@ require_once('../approot.inc.php');
// check if header contains X-Combodo-Ajax for POST request (CSRF protection for ajax calls)
-/*if (!isset($_SERVER['HTTP_X_COMBODO_AJAX']) && $_SERVER['REQUEST_METHOD'] === 'POST') {
+if (!isset($_SERVER['HTTP_X_COMBODO_AJAX']) && $_SERVER['REQUEST_METHOD'] === 'POST') {
$sReferer = utils::HtmlEntities($_SERVER['HTTP_REFERER']);
IssueLog::Error("Unprotected ajax call from: $sReferer", 'SECURITY');
header('HTTP/1.1 401 Unauthorized');
die('Unauthorized access. Please see https://www.itophub.io/wiki/page?id=3_2_0:release:developer#checking_for_the_presence_of_specific_header_in_the_post_to_enhance_protection_against_csrf_attacks');
-}*/
+}
function LogErrorMessage($sMsgPrefix, $aContextInfo) {
diff --git a/sources/Application/WebPage/NiceWebPage.php b/sources/Application/WebPage/NiceWebPage.php
index e8e0051ef..4e105a5dc 100644
--- a/sources/Application/WebPage/NiceWebPage.php
+++ b/sources/Application/WebPage/NiceWebPage.php
@@ -153,6 +153,7 @@ JS
// Used throughout the app.
$this->LinkScriptFromAppRoot('node_modules/jquery/dist/jquery.min.js');
+ $this->LinkScriptFromAppRoot('js/ajax_hook.js');
$this->LinkScriptFromAppRoot('js/jquery.blockUI.js');
if (utils::IsDevelopmentEnvironment()) // Needed since many other plugins still rely on oldies like $.browser
{
diff --git a/sources/Controller/OAuth/OAuthLandingController.php b/sources/Controller/OAuth/OAuthLandingController.php
index fd0e03807..a6429738c 100644
--- a/sources/Controller/OAuth/OAuthLandingController.php
+++ b/sources/Controller/OAuth/OAuthLandingController.php
@@ -10,6 +10,7 @@ class OAuthLandingController extends Controller
public function OperationLanding()
{
$this->AddLinkedScript(utils::GetAbsoluteUrlAppRoot().'node_modules/jquery/dist/jquery.min.js');
+ $this->AddLinkedScript(utils::GetAbsoluteUrlAppRoot().'js/ajax_hook.js');
$this->DisplayPage([], null, static::ENUM_PAGE_TYPE_BASIC_HTML);
}
}
\ No newline at end of file
From ab9df22e76bbb729c01d007abd9a041ae0674578 Mon Sep 17 00:00:00 2001
From: Romain Quetiez
Date: Tue, 2 Jul 2024 16:55:16 +0200
Subject: [PATCH 2/2] :memo: Reinforce the documentation of WebPage JS
inclusion API
---
sources/Application/WebPage/WebPage.php | 87 +++++++++++++++++++++----
1 file changed, 74 insertions(+), 13 deletions(-)
diff --git a/sources/Application/WebPage/WebPage.php b/sources/Application/WebPage/WebPage.php
index ba442ff85..b984cd597 100644
--- a/sources/Application/WebPage/WebPage.php
+++ b/sources/Application/WebPage/WebPage.php
@@ -47,6 +47,17 @@ use const MODULESROOT;
* $oPage->p("Hello World !");
* $oPage->output();
* ```
+ *
+ *
+ * JS scripts can be added. They will be executed in sequence depending on the API used to inject them:
+ *
+ * - add_early_script()
+ * - LinkScriptFromAppRoot(), LinkScriptFromURI(), LinkScriptFromModule()
+ * - add_script()
+ * - add_init_script() - DOMContentLoaded
+ * - add_ready_script() - DOMContentLoaded + 50 ms
+ *
+ *
*/
class WebPage implements Page
{
@@ -660,12 +671,21 @@ class WebPage implements Page
}
/**
- * Add some Javascript to the header of the page, therefore executed first, BEFORE the DOM interpretation.
+ * Add some Javascript to the header of the page
+ *
+ * The provided JS code will be executed at step 1 of the JS execution chain:
+ * [early script] ==> linked script ==> script ==> init script ==> ready script
+ *
* /!\ Keep in mind that no external JS files (eg. jQuery) will be loaded yet.
*
- * @uses WebPage::$a_a_early_scripts
+ * @api-advanced
+ * @see static::add_script, static::add_init_script, static::add_ready_script
+ * @see static::LinkScriptFromAppRoot, static::LinkScriptFromModule, static::LinkScriptFromURI
+ *
* @param string $s_script
+ *
* @since 3.0.0
+ * @uses WebPage::$a_early_scripts
*/
public function add_early_script($s_script)
{
@@ -701,8 +721,16 @@ class WebPage implements Page
/**
* Add some Javascript to be executed immediately without waiting for the DOM to be ready
*
- * @uses WebPage::$a_scripts
+ * The provided JS code will be executed at step 3 of the JS execution chain:
+ * early script ==> linked script ==> [script] ==> init script ==> ready script
+ *
+ * @api-advanced
+ * @see static::add_early_script, static::add_init_script, static::add_ready_script
+ * @see static::LinkScriptFromAppRoot, static::LinkScriptFromURI, static::LinkScriptFromModule
+ *
* @param string $s_script
+ *
+ * @uses WebPage::$a_scripts
* @since 3.0.0 These scripts are put at the end of the tag instead of the end of the tag, {@see static::add_early_script} to add script there
*/
public function add_script($s_script)
@@ -737,12 +765,19 @@ class WebPage implements Page
}
/**
- * Adds a script to be executed when the DOM is ready (typical JQuery use), right before add_ready_script
+ * Add a script to be executed when the DOM is ready (typical JQuery use)
+ *
+ * The provided JS code will be executed at step 4 of the JS execution chain:
+ * early script ==> linked script ==> script ==> [init script] ==> ready script
+ *
+ * @api-advanced
+ * @see static::add_early_script, static::add_script, static::add_ready_script
+ * @see static::LinkScriptFromAppRoot, static::LinkScriptFromURI, static::LinkScriptFromModule
*
- * @uses WebPage::$a_init_scripts
* @param string $sScript
*
* @return void
+ *@uses WebPage::$a_init_scripts
*/
public function add_init_script($sScript)
{
@@ -776,10 +811,18 @@ class WebPage implements Page
}
/**
- * Add some Javascript to be executed once the DOM is ready, slightly after the "init scripts"
+ * Add some Javascript to be executed once the DOM is ready, slightly (50 ms) after the "init scripts"
+ *
+ * The provided JS code will be executed at step 5 of the JS execution chain:
+ * early script ==> linked script ==> script ==> init script ==> [ready script]
+ *
+ * @api-advanced
+ * @see static::add_early_script, static::add_script, static::add_init_script
+ * @see static::LinkScriptFromAppRoot, static::LinkScriptFromURI, static::LinkScriptFromModule
+ *
+ * @param $sScript
*
* @uses WebPage::$a_ready_scripts
- * @param $sScript
*/
public function add_ready_script($sScript)
{
@@ -886,14 +929,20 @@ class WebPage implements Page
}
/**
- * Use to link JS files from the iTop package (e.g. `/js/*`)
+ * Use to include JS files from the iTop package (e.g. `/js/*`)
+ *
+ * The provided JS code will be executed at step 2 of the JS execution chain:
+ * early script ==> [linked script] ==> script ==> init script ==> ready script
+ *
+ * @api-advanced
+ * @see static::add_early_script, static::add_script, static::add_init_script, static::add_ready_script
+ * @see static::LinkScriptFromURI, static::LinkScriptFromModule
*
* @param string $sFileRelPath Rel. path from iTop app. root of the JS file to link (e.g. `js/utils.js`)
*
* @return void
* @throws \Exception
* @since 3.2.0 N°7315
- * @api
*/
public function LinkScriptFromAppRoot(string $sFileRelPath): void
{
@@ -901,14 +950,20 @@ class WebPage implements Page
}
/**
- * Use to link JS files from any module
+ * Use to include JS files from any module
+ *
+ * The provided JS code will be executed at step 2 of the JS execution chain:
+ * early script ==> [linked script] ==> script ==> init script ==> ready script
+ *
+ * @api-advanced
+ * @see static::add_early_script, static::add_script, static::add_init_script, static::add_ready_script
+ * @see static::LinkScriptFromAppRoot, static::LinkScriptFromURI
*
* @param string $sFileRelPath Rel. path from current environment (e.g. `/env-production/`) of the JS file to link (e.g. `some-module/asset/js/some-file.js`)
*
* @return void
* @throws \Exception
* @since 3.2.0 N°7315
- * @api
*/
public function LinkScriptFromModule(string $sFileRelPath): void
{
@@ -916,7 +971,14 @@ class WebPage implements Page
}
/**
- * Use to link JS files from any URI, typically an external server
+ * Use to include JS files from any URI, typically an external server
+ *
+ * The provided JS code will be executed at step 2 of the JS execution chain:
+ * early script ==> [linked script] ==> script ==> init script ==> ready script
+ *
+ * @api-advanced
+ * @see static::add_early_script, static::add_script, static::add_init_script, static::add_ready_script
+ * @see static::LinkScriptFromAppRoot, static::LinkScriptFromModule
*
* @param string $sFileAbsURI Abs. URI of the JS file to link (e.g. `https://external.server.com/some-file.js`)
* Note: Any non-absolute URI will be ignored.
@@ -924,7 +986,6 @@ class WebPage implements Page
* @return void
* @throws \Exception
* @since 3.2.0 N°7315
- * @api
*/
public function LinkScriptFromURI(string $sFileAbsURI): void
{