From 7588211732827e9d3b91b8aa8b9b480018ca7fff Mon Sep 17 00:00:00 2001 From: aynsix Date: Fri, 4 Aug 2023 17:41:26 +0300 Subject: [PATCH 01/16] csrf token form --- .../dist/production.js | 12 +++++-- .../dist/production.min.js | 12 +++++-- .../src/components/record/delete.js | 11 +++++-- lib/Alchemy/Phrasea/Controller/Controller.php | 31 +++++++++++++++++++ .../Controller/Prod/DownloadController.php | 4 +++ .../Controller/Prod/ExportController.php | 13 ++++++++ .../Controller/Prod/QueryController.php | 5 +++ .../Controller/Prod/RecordController.php | 6 ++++ .../Controller/Prod/RootController.php | 2 ++ .../Controller/Prod/ToolsController.php | 21 +++++++++++++ .../Order/Controller/ProdOrderController.php | 4 +++ templates/web/common/dialog_export.html.twig | 12 ++++--- .../web/prod/actions/Tools/index.html.twig | 9 ++++-- .../delete_records_confirm_form.html.twig | 1 + templates/web/prod/index.html.twig | 1 + 15 files changed, 130 insertions(+), 14 deletions(-) diff --git a/Phraseanet-production-client/dist/production.js b/Phraseanet-production-client/dist/production.js index 63f26f123f..bae34931cf 100644 --- a/Phraseanet-production-client/dist/production.js +++ b/Phraseanet-production-client/dist/production.js @@ -62256,6 +62256,7 @@ var deleteRecord = function deleteRecord(services) { $trash_counter = $form.find(".to_trash_count"), $loader = $form.find(".form-action-loader"); var lst = (0, _jquery2.default)("input[name='lst']", $form).val().split(';'); + var csrfToken = (0, _jquery2.default)("input[name='prodDeleteRecord_token']", $form).val(); /** * same parameters for every delete call, except the list of (CHUNKSIZE) records @@ -62265,9 +62266,10 @@ var deleteRecord = function deleteRecord(services) { type: $form.attr("method"), url: $form.attr("action"), data: { - 'lst': "" // set in f + lst: '', // set in f + prodDeleteRecord_token: csrfToken }, - dataType: "json" + dataType: 'json' }; var runningTasks = 0, @@ -62293,7 +62295,11 @@ var deleteRecord = function deleteRecord(services) { } // pop & truncate ajaxParms.data.lst = lst.splice(0, CHUNKSIZE).join(';'); - _jquery2.default.ajax(ajaxParms).success(function (data) { + _jquery2.default.ajax(ajaxParms).error(function (data) { + fCancel(); + $dialog.close(); + alert('invalid csrf token delete form'); + }).success(function (data) { // prod feedback only if result ok _jquery2.default.each(data, function (i, n) { var imgt = (0, _jquery2.default)('#IMGT_' + n), diff --git a/Phraseanet-production-client/dist/production.min.js b/Phraseanet-production-client/dist/production.min.js index 63f26f123f..bae34931cf 100644 --- a/Phraseanet-production-client/dist/production.min.js +++ b/Phraseanet-production-client/dist/production.min.js @@ -62256,6 +62256,7 @@ var deleteRecord = function deleteRecord(services) { $trash_counter = $form.find(".to_trash_count"), $loader = $form.find(".form-action-loader"); var lst = (0, _jquery2.default)("input[name='lst']", $form).val().split(';'); + var csrfToken = (0, _jquery2.default)("input[name='prodDeleteRecord_token']", $form).val(); /** * same parameters for every delete call, except the list of (CHUNKSIZE) records @@ -62265,9 +62266,10 @@ var deleteRecord = function deleteRecord(services) { type: $form.attr("method"), url: $form.attr("action"), data: { - 'lst': "" // set in f + lst: '', // set in f + prodDeleteRecord_token: csrfToken }, - dataType: "json" + dataType: 'json' }; var runningTasks = 0, @@ -62293,7 +62295,11 @@ var deleteRecord = function deleteRecord(services) { } // pop & truncate ajaxParms.data.lst = lst.splice(0, CHUNKSIZE).join(';'); - _jquery2.default.ajax(ajaxParms).success(function (data) { + _jquery2.default.ajax(ajaxParms).error(function (data) { + fCancel(); + $dialog.close(); + alert('invalid csrf token delete form'); + }).success(function (data) { // prod feedback only if result ok _jquery2.default.each(data, function (i, n) { var imgt = (0, _jquery2.default)('#IMGT_' + n), diff --git a/Phraseanet-production-client/src/components/record/delete.js b/Phraseanet-production-client/src/components/record/delete.js index 10f2aefe8a..e6ed7eddc3 100644 --- a/Phraseanet-production-client/src/components/record/delete.js +++ b/Phraseanet-production-client/src/components/record/delete.js @@ -76,6 +76,7 @@ const deleteRecord = (services) => { $trash_counter = $form.find(".to_trash_count"), $loader = $form.find(".form-action-loader"); let lst = $("input[name='lst']", $form).val().split(';'); + let csrfToken = $("input[name='prodDeleteRecord_token']", $form).val(); /** * same parameters for every delete call, except the list of (CHUNKSIZE) records @@ -85,9 +86,10 @@ const deleteRecord = (services) => { type: $form.attr("method"), url: $form.attr("action"), data: { - 'lst': "" // set in f + lst: '', // set in f + prodDeleteRecord_token: csrfToken }, - dataType: "json" + dataType: 'json' }; let runningTasks = 0, // number of running tasks @@ -113,6 +115,11 @@ const deleteRecord = (services) => { // pop & truncate ajaxParms.data.lst = lst.splice(0, CHUNKSIZE).join(';'); $.ajax(ajaxParms) + .error(function (data) { + fCancel(); + $dialog.close(); + alert('invalid csrf token delete form'); + }) .success(function (data) { // prod feedback only if result ok $.each(data, function (i, n) { let imgt = $('#IMGT_' + n), diff --git a/lib/Alchemy/Phrasea/Controller/Controller.php b/lib/Alchemy/Phrasea/Controller/Controller.php index 79890b3501..97919b3f0f 100644 --- a/lib/Alchemy/Phrasea/Controller/Controller.php +++ b/lib/Alchemy/Phrasea/Controller/Controller.php @@ -15,7 +15,9 @@ use Alchemy\Phrasea\Authentication\Authenticator; use Alchemy\Phrasea\Core\Configuration\PropertyAccess; use Alchemy\Phrasea\Model\Entities\User; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Session\Session; class Controller { @@ -112,6 +114,35 @@ public function getAuthenticatedUser() return $this->getAuthenticator()->getUser(); } + public function setSessionFormToken($formName) + { + $randomValue = bin2hex(random_bytes(35)); + $this->app['session']->set($formName.'_token', $randomValue); + + return $randomValue; + } + + public function getSessionFormToken($formName) + { + return $this->app['session']->get($formName.'_token'); + } + + public function isCrsfValid(Request $request, $formName) + { + if (!$request->isMethod("POST")) { + return false; + } + + $formTokenName = $formName . '_token'; + $formToken = (string) $request->request->get($formTokenName); + + if (empty($formToken) || $formToken != $this->getSessionFormToken($formName)) { + return false; + } + + return true; + } + /** * @return PropertyAccess */ diff --git a/lib/Alchemy/Phrasea/Controller/Prod/DownloadController.php b/lib/Alchemy/Phrasea/Controller/Prod/DownloadController.php index 9d19d7297c..c575c86431 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/DownloadController.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/DownloadController.php @@ -29,6 +29,10 @@ class DownloadController extends Controller */ public function checkDownload(Request $request) { + if (!$this->isCrsfValid($request, 'prodExportDownload')) { + $this->app->abort(403); + } + $lst = $request->request->get('lst'); $ssttid = $request->request->get('ssttid', ''); $subdefs = $request->request->get('obj', []); diff --git a/lib/Alchemy/Phrasea/Controller/Prod/ExportController.php b/lib/Alchemy/Phrasea/Controller/Prod/ExportController.php index 50db492c2c..996796bcb8 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/ExportController.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/ExportController.php @@ -44,6 +44,11 @@ public function displayMultiExport(Request $request) $request->request->get('story') ); + $this->setSessionFormToken('prodExportDownload'); + $this->setSessionFormToken('prodExportEmail'); + $this->setSessionFormToken('prodExportFTP'); + $this->setSessionFormToken('prodExportOrder'); + return new Response($this->render('common/dialog_export.html.twig', [ 'download' => $download, 'ssttid' => $request->request->get('ssel'), @@ -90,6 +95,10 @@ public function testFtpConnexion(Request $request) */ public function exportFtp(Request $request) { + if (!$this->isCrsfValid($request, 'prodExportFTP')) { + return $this->app->json(['message' => 'invalid export ftp form'], 403); + } + $download = new \set_exportftp($this->app, $request->request->get('lst'), $request->request->get('ssttid')); $mandatoryParameters = ['address', 'login', 'obj']; @@ -153,6 +162,10 @@ public function exportFtp(Request $request) */ public function exportMail(Request $request) { + if (!$this->isCrsfValid($request, 'prodExportEmail')) { + return $this->app->json(['message' => 'invalid export mail form'], 403); + } + set_time_limit(0); session_write_close(); ignore_user_abort(true); diff --git a/lib/Alchemy/Phrasea/Controller/Prod/QueryController.php b/lib/Alchemy/Phrasea/Controller/Prod/QueryController.php index d055206d18..9ae6b379e4 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/QueryController.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/QueryController.php @@ -21,6 +21,7 @@ use Alchemy\Phrasea\Utilities\StringHelper; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use unicode; class QueryController extends Controller @@ -121,6 +122,10 @@ public function completion(Request $request) */ public function query(Request $request) { + if (!$this->isCrsfValid($request, 'searchForm')) { + return $this->app->json(['message' => 'invalid search token'], 403); + } + $query = (string) $request->request->get('qry'); // since the query comes from a submited form, normalize crlf,cr,lf ... diff --git a/lib/Alchemy/Phrasea/Controller/Prod/RecordController.php b/lib/Alchemy/Phrasea/Controller/Prod/RecordController.php index 3c4e09ad75..bc55f20a40 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/RecordController.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/RecordController.php @@ -224,6 +224,10 @@ public function getRecordById($sbasId, $recordId) */ public function doDeleteRecords(Request $request) { + if ($this->isCrsfValid($request, 'prodDeleteRecord')) { + return $this->app->json(['success' => false , 'message' => 'invalid delete form'], 403); + } + $records = RecordsRequest::fromRequest( $this->app, $request, @@ -351,6 +355,8 @@ public function whatCanIDelete(Request $request) 'deletableCount' => count($filteredRecords['delete']) ]; + $this->setSessionFormToken('prodDeleteRecord'); + return $this->render( 'prod/actions/delete_records_confirm.html.twig', $viewParms diff --git a/lib/Alchemy/Phrasea/Controller/Prod/RootController.php b/lib/Alchemy/Phrasea/Controller/Prod/RootController.php index b89c919ed9..2052795756 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/RootController.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/RootController.php @@ -104,6 +104,8 @@ public function indexAction(Request $request) { 'actionbar' => $filter('actionbar'), ]; + $this->setSessionFormToken('searchForm'); + return $this->render('prod/index.html.twig', [ 'module_name' => 'Production', 'WorkZone' => new WorkzoneHelper($this->app, $request), diff --git a/lib/Alchemy/Phrasea/Controller/Prod/ToolsController.php b/lib/Alchemy/Phrasea/Controller/Prod/ToolsController.php index 3987296e0c..6a30a0c055 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/ToolsController.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/ToolsController.php @@ -113,6 +113,11 @@ public function indexAction(Request $request) } } + $this->setSessionFormToken('prodToolsSubdef'); + $this->setSessionFormToken('prodToolsRotate'); + $this->setSessionFormToken('prodToolsHDSubstitution'); + $this->setSessionFormToken('prodToolsThumbSubstitution'); + return $this->render('prod/actions/Tools/index.html.twig', [ 'records' => $records, 'record' => $record, @@ -127,6 +132,10 @@ public function indexAction(Request $request) public function rotateAction(Request $request) { + if (!$this->isCrsfValid($request, 'prodToolsRotate')) { + return $this->app->json(['success' => false , 'message' => 'invalid rotate form'], 403); + } + $records = RecordsRequest::fromRequest($this->app, $request, false); $rotation = (int)$request->request->get('rotation', 90); $rotation %= 360; @@ -165,6 +174,10 @@ public function rotateAction(Request $request) public function imageAction(Request $request) { + if (!$this->isCrsfValid($request, 'prodToolsSubdef')) { + return $this->app->json(['success' => false , 'message' => 'invalid create subview form'], 403); + } + $return = ['success' => true]; $force = $request->request->get('force_substitution') == '1'; @@ -211,6 +224,10 @@ public function imageAction(Request $request) public function hddocAction(Request $request) { + if (!$this->isCrsfValid($request, 'prodToolsHDSubstitution')) { + return $this->app->json(['success' => false , 'message' => 'invalid document substitution form'], 403); + } + $success = false; $message = $this->app->trans('An error occured'); @@ -269,6 +286,10 @@ public function hddocAction(Request $request) public function changeThumbnailAction(Request $request) { + if (!$this->isCrsfValid($request, 'prodToolsThumbSubstitution')) { + return $this->app->json(['success' => false , 'message' => 'invalid thumbnail substitution form'], 403); + } + $file = $request->files->get('newThumb'); if (empty($file)) { diff --git a/lib/Alchemy/Phrasea/Order/Controller/ProdOrderController.php b/lib/Alchemy/Phrasea/Order/Controller/ProdOrderController.php index ac176bab42..976ab8611b 100644 --- a/lib/Alchemy/Phrasea/Order/Controller/ProdOrderController.php +++ b/lib/Alchemy/Phrasea/Order/Controller/ProdOrderController.php @@ -37,6 +37,10 @@ class ProdOrderController extends BaseOrderController */ public function createOrder(Request $request) { + if (!$this->isCrsfValid($request, 'prodExportOrder')) { + return $this->app->json(['message' => 'invalid export order form'], 403); + } + $records = RecordsRequest::fromRequest($this->app, $request, true, [\ACL::CANCMD]); try { diff --git a/templates/web/common/dialog_export.html.twig b/templates/web/common/dialog_export.html.twig index b4faf0029f..b99a837708 100644 --- a/templates/web/common/dialog_export.html.twig +++ b/templates/web/common/dialog_export.html.twig @@ -108,7 +108,7 @@

{{ 'export:: telechargement' | trans }}

-
+ {% for name, values in download.get_display_download() %} @@ -168,13 +168,14 @@
+

{{ 'export:: envoi par mail' | trans }}

-
+
@@ -257,6 +258,7 @@
+
@@ -296,7 +298,7 @@ {% endif %} {% endfor %}
-
+ @@ -404,6 +406,7 @@ +
{% endif %} @@ -430,7 +433,7 @@ {% endfor %} -
+
@@ -479,6 +482,7 @@
{% endif %} +
diff --git a/templates/web/prod/actions/Tools/index.html.twig b/templates/web/prod/actions/Tools/index.html.twig index d4fa372ec0..2df6f62224 100644 --- a/templates/web/prod/actions/Tools/index.html.twig +++ b/templates/web/prod/actions/Tools/index.html.twig @@ -61,7 +61,7 @@
{# subdef section #}
-
+
 {{ "Reconstruire les sous definitions" | trans }}  {% if nbSubdefSubstitute > 0 %} @@ -107,7 +107,9 @@
{{ 'prod::tool:recreatesubviews: warning for rebuild sub-definitions' | trans }} -
+ +
+ @@ -136,6 +138,7 @@ + @@ -176,6 +179,7 @@ +
@@ -202,6 +206,7 @@ +
diff --git a/templates/web/prod/actions/delete_records_confirm_form.html.twig b/templates/web/prod/actions/delete_records_confirm_form.html.twig index 91beb1b15f..fc20b73862 100644 --- a/templates/web/prod/actions/delete_records_confirm_form.html.twig +++ b/templates/web/prod/actions/delete_records_confirm_form.html.twig @@ -37,6 +37,7 @@ + {% elseif records.received().count() == 0 %}
diff --git a/templates/web/prod/index.html.twig b/templates/web/prod/index.html.twig index 4e2a92daf3..a7dba2e875 100644 --- a/templates/web/prod/index.html.twig +++ b/templates/web/prod/index.html.twig @@ -744,6 +744,7 @@
+ From b5c99f082699d640c7a8fd6d4ed38630f7946774 Mon Sep 17 00:00:00 2001 From: aynsix Date: Fri, 4 Aug 2023 18:23:02 +0300 Subject: [PATCH 02/16] add csrf token --- Phraseanet-production-client/config/config.js | 2 +- Phraseanet-production-client/dist/authenticate.js | 2 +- .../dist/authenticate.min.js | 2 +- Phraseanet-production-client/dist/commons.js | 2 +- Phraseanet-production-client/dist/commons.min.js | 2 +- .../Phrasea/Controller/Prod/PropertyController.php | 12 ++++++++++++ lib/Alchemy/Phrasea/Twig/PhraseanetExtension.php | 2 +- templates/web/prod/actions/Property/index.html.twig | 1 + templates/web/prod/actions/Property/type.html.twig | 1 + 9 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Phraseanet-production-client/config/config.js b/Phraseanet-production-client/config/config.js index 7b1bdbf13f..208c4dd76c 100644 --- a/Phraseanet-production-client/config/config.js +++ b/Phraseanet-production-client/config/config.js @@ -13,5 +13,5 @@ module.exports = { setupDir: _root + 'tests/setup/node.js', karmaConf: _root + 'config/karma.conf.js', // change this version when you change JS file for lazy loading - assetFileVersion: 92 + assetFileVersion: 93 }; diff --git a/Phraseanet-production-client/dist/authenticate.js b/Phraseanet-production-client/dist/authenticate.js index eef05c259d..d95caa7394 100644 --- a/Phraseanet-production-client/dist/authenticate.js +++ b/Phraseanet-production-client/dist/authenticate.js @@ -96,7 +96,7 @@ return /******/ (function(modules) { // webpackBootstrap /******/ if (__webpack_require__.nc) { /******/ script.setAttribute("nonce", __webpack_require__.nc); /******/ } -/******/ script.src = __webpack_require__.p + "lazy-" + ({}[chunkId]||chunkId) + ".js?v=92"; +/******/ script.src = __webpack_require__.p + "lazy-" + ({}[chunkId]||chunkId) + ".js?v=93"; /******/ var timeout = setTimeout(onScriptComplete, 120000); /******/ script.onerror = script.onload = onScriptComplete; /******/ function onScriptComplete() { diff --git a/Phraseanet-production-client/dist/authenticate.min.js b/Phraseanet-production-client/dist/authenticate.min.js index 1e11703f65..d4069e03e9 100644 --- a/Phraseanet-production-client/dist/authenticate.min.js +++ b/Phraseanet-production-client/dist/authenticate.min.js @@ -96,7 +96,7 @@ return /******/ (function(modules) { // webpackBootstrap /******/ if (__webpack_require__.nc) { /******/ script.setAttribute("nonce", __webpack_require__.nc); /******/ } -/******/ script.src = __webpack_require__.p + "lazy-" + ({}[chunkId]||chunkId) + ".min.js?v=92"; +/******/ script.src = __webpack_require__.p + "lazy-" + ({}[chunkId]||chunkId) + ".min.js?v=93"; /******/ var timeout = setTimeout(onScriptComplete, 120000); /******/ script.onerror = script.onload = onScriptComplete; /******/ function onScriptComplete() { diff --git a/Phraseanet-production-client/dist/commons.js b/Phraseanet-production-client/dist/commons.js index 72933035c0..e4393dad66 100644 --- a/Phraseanet-production-client/dist/commons.js +++ b/Phraseanet-production-client/dist/commons.js @@ -91,7 +91,7 @@ /******/ if (__webpack_require__.nc) { /******/ script.setAttribute("nonce", __webpack_require__.nc); /******/ } -/******/ script.src = __webpack_require__.p + "lazy-" + ({}[chunkId]||chunkId) + ".js?v=92"; +/******/ script.src = __webpack_require__.p + "lazy-" + ({}[chunkId]||chunkId) + ".js?v=93"; /******/ var timeout = setTimeout(onScriptComplete, 120000); /******/ script.onerror = script.onload = onScriptComplete; /******/ function onScriptComplete() { diff --git a/Phraseanet-production-client/dist/commons.min.js b/Phraseanet-production-client/dist/commons.min.js index 4671744f14..c52c6a587d 100644 --- a/Phraseanet-production-client/dist/commons.min.js +++ b/Phraseanet-production-client/dist/commons.min.js @@ -91,7 +91,7 @@ /******/ if (__webpack_require__.nc) { /******/ script.setAttribute("nonce", __webpack_require__.nc); /******/ } -/******/ script.src = __webpack_require__.p + "lazy-" + ({}[chunkId]||chunkId) + ".min.js?v=92"; +/******/ script.src = __webpack_require__.p + "lazy-" + ({}[chunkId]||chunkId) + ".min.js?v=93"; /******/ var timeout = setTimeout(onScriptComplete, 120000); /******/ script.onerror = script.onload = onScriptComplete; /******/ function onScriptComplete() { diff --git a/lib/Alchemy/Phrasea/Controller/Prod/PropertyController.php b/lib/Alchemy/Phrasea/Controller/Prod/PropertyController.php index 74022f1422..a67a221613 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/PropertyController.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/PropertyController.php @@ -33,6 +33,8 @@ public function displayStatusProperty(Request $request) $records = RecordsRequest::fromRequest($this->app, $request, false, [\ACL::CHGSTATUS]); + $this->setSessionFormToken('prodPropertyStatus'); + $databoxes = $records->databoxes(); if (count($databoxes) > 1) { return new Response($this->render('prod/actions/Property/index.html.twig', [ @@ -103,6 +105,8 @@ public function displayTypeProperty(Request $request) $recordsType[$sbasId][$record->getType()][] = $record; } + $this->setSessionFormToken('prodPropertyType'); + return new Response($this->render('prod/actions/Property/type.html.twig', [ 'records' => $records, 'recordsType' => $recordsType, @@ -117,6 +121,10 @@ public function displayTypeProperty(Request $request) */ public function changeStatus(Request $request) { + if (!$this->isCrsfValid($request, 'prodPropertyStatus')) { + return $this->app->json(['message' => 'invalid change status form'], 403); + } + $applyStatusToChildren = $request->request->get('apply_to_children', []); $records = RecordsRequest::fromRequest($this->app, $request, false, [\ACL::CHGSTATUS]); $updated = []; @@ -151,6 +159,10 @@ public function changeStatus(Request $request) */ public function changeType(Request $request) { + if (!$this->isCrsfValid($request, 'prodPropertyType')) { + return $this->app->json(['message' => 'invalid change type form'], 403); + } + $typeLst = $request->request->get('types', []); $records = RecordsRequest::fromRequest($this->app, $request, false, [\ACL::CANMODIFRECORD]); $mimeLst = $request->request->get('mimes', []); diff --git a/lib/Alchemy/Phrasea/Twig/PhraseanetExtension.php b/lib/Alchemy/Phrasea/Twig/PhraseanetExtension.php index 34f120f19e..a1936c09ed 100644 --- a/lib/Alchemy/Phrasea/Twig/PhraseanetExtension.php +++ b/lib/Alchemy/Phrasea/Twig/PhraseanetExtension.php @@ -62,7 +62,7 @@ public function getGlobals() { return [ // change this version when you change JS file to force the navigation to reload js file - 'assetFileVersion' => 92 + 'assetFileVersion' => 93 ]; } diff --git a/templates/web/prod/actions/Property/index.html.twig b/templates/web/prod/actions/Property/index.html.twig index f3abc7d6c3..1525aca1f0 100644 --- a/templates/web/prod/actions/Property/index.html.twig +++ b/templates/web/prod/actions/Property/index.html.twig @@ -116,6 +116,7 @@ +
diff --git a/templates/web/prod/actions/Property/type.html.twig b/templates/web/prod/actions/Property/type.html.twig index 4ce19322a0..e33c5f640c 100644 --- a/templates/web/prod/actions/Property/type.html.twig +++ b/templates/web/prod/actions/Property/type.html.twig @@ -52,4 +52,5 @@ + From 1a84d79077179f5854bd91e4ba2cb23c1a11d0aa Mon Sep 17 00:00:00 2001 From: aynsix Date: Mon, 7 Aug 2023 18:59:35 +0300 Subject: [PATCH 03/16] add csrf --- Phraseanet-production-client/dist/production.js | 3 ++- Phraseanet-production-client/dist/production.min.js | 3 ++- Phraseanet-production-client/src/components/record/move.js | 3 ++- .../Phrasea/Controller/Prod/MoveCollectionController.php | 6 ++++++ lib/Alchemy/Phrasea/Controller/Prod/PushController.php | 6 ++++++ templates/web/prod/actions/Push.html.twig | 1 + templates/web/prod/actions/collection_default.html.twig | 1 + 7 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Phraseanet-production-client/dist/production.js b/Phraseanet-production-client/dist/production.js index bae34931cf..9c638265c8 100644 --- a/Phraseanet-production-client/dist/production.js +++ b/Phraseanet-production-client/dist/production.js @@ -21998,7 +21998,8 @@ var moveRecord = function moveRecord(services) { var datas = { lst: (0, _jquery2.default)('input[name="lst"]', $form).val(), base_id: (0, _jquery2.default)('select[name="base_id"]', $form).val(), - chg_coll_son: coll_son + chg_coll_son: coll_son, + prodMoveCollection_token: (0, _jquery2.default)('input[name="prodMoveCollection_token"]', $form).val() }; var buttonPanel = $dialog.getDomElement().closest('.ui-dialog').find('.ui-dialog-buttonpane'); diff --git a/Phraseanet-production-client/dist/production.min.js b/Phraseanet-production-client/dist/production.min.js index bae34931cf..9c638265c8 100644 --- a/Phraseanet-production-client/dist/production.min.js +++ b/Phraseanet-production-client/dist/production.min.js @@ -21998,7 +21998,8 @@ var moveRecord = function moveRecord(services) { var datas = { lst: (0, _jquery2.default)('input[name="lst"]', $form).val(), base_id: (0, _jquery2.default)('select[name="base_id"]', $form).val(), - chg_coll_son: coll_son + chg_coll_son: coll_son, + prodMoveCollection_token: (0, _jquery2.default)('input[name="prodMoveCollection_token"]', $form).val() }; var buttonPanel = $dialog.getDomElement().closest('.ui-dialog').find('.ui-dialog-buttonpane'); diff --git a/Phraseanet-production-client/src/components/record/move.js b/Phraseanet-production-client/src/components/record/move.js index 77ed60b9a5..3e2d840b34 100644 --- a/Phraseanet-production-client/src/components/record/move.js +++ b/Phraseanet-production-client/src/components/record/move.js @@ -48,7 +48,8 @@ const moveRecord = (services) => { var datas = { lst: $('input[name="lst"]', $form).val(), base_id: $('select[name="base_id"]', $form).val(), - chg_coll_son: coll_son + chg_coll_son: coll_son, + prodMoveCollection_token: $('input[name="prodMoveCollection_token"]', $form).val() }; var buttonPanel = $dialog.getDomElement() diff --git a/lib/Alchemy/Phrasea/Controller/Prod/MoveCollectionController.php b/lib/Alchemy/Phrasea/Controller/Prod/MoveCollectionController.php index 274ab2ac17..23ff3c2b83 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/MoveCollectionController.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/MoveCollectionController.php @@ -62,6 +62,8 @@ public function displayForm(Request $request) // sort the collections array_multisort($aName, $uorder, SORT_REGULAR, $collections); + $this->setSessionFormToken('prodMoveCollection'); + $parameters = [ 'records' => $records, 'message' => '', @@ -81,6 +83,10 @@ public function displayForm(Request $request) public function apply(Request $request) { + if (!$this->isCrsfValid($request, 'prodMoveCollection')) { + return $this->app->json(['success' => false, 'message' => 'invalid move collection form']); + } + /** @var \record_adapter[] $records */ $records = RecordsRequest::fromRequest($this->app, $request, false, [\ACL::CANDELETERECORD]); diff --git a/lib/Alchemy/Phrasea/Controller/Prod/PushController.php b/lib/Alchemy/Phrasea/Controller/Prod/PushController.php index 796b4f80ba..9d9b063018 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/PushController.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/PushController.php @@ -215,6 +215,10 @@ public function sendAction(Request $request) */ public function sharebasketAction(Request $request) { + if (!$this->isCrsfValid($request, 'prodShareBasket')) { + return $this->app->json(['success' => false, 'message' => 'invalid form']); + } + $ret = [ 'success' => false, 'message' => $this->app->trans('Unable to send the documents') @@ -817,6 +821,8 @@ public function renderPushTemplate(Request $request, $context) $repository = $this->getUserListRepository(); $recommendedUsers = $this->getUsersInSelectionExtractor($push->get_elements()); + $this->setSessionFormToken('prodShareBasket'); + return $this->render( 'prod/actions/Push.html.twig', [ diff --git a/templates/web/prod/actions/Push.html.twig b/templates/web/prod/actions/Push.html.twig index 86a22989b9..4eb20d39b0 100644 --- a/templates/web/prod/actions/Push.html.twig +++ b/templates/web/prod/actions/Push.html.twig @@ -508,6 +508,7 @@ {% endif %} + From c9f98f51aef0f92f64dc74c606c0c54ec9dc4d5c Mon Sep 17 00:00:00 2001 From: aynsix Date: Wed, 9 Aug 2023 18:05:18 +0300 Subject: [PATCH 04/16] add csrf --- .../Controller/Prod/BasketController.php | 18 ++++++++++ .../Controller/Prod/PrinterController.php | 6 ++++ .../Controller/Prod/PushController.php | 6 ++++ .../Controller/Prod/StoryController.php | 12 +++++++ templates/web/prod/Baskets/Create.html.twig | 1 + templates/web/prod/Baskets/Reorder.html.twig | 33 ++++++++++--------- templates/web/prod/Baskets/Update.html.twig | 1 + templates/web/prod/Story/Create.html.twig | 2 +- templates/web/prod/Story/Reorder.html.twig | 1 + templates/web/prod/User/Add.html.twig | 1 + .../prod/actions/printer_default.html.twig | 3 +- 11 files changed, 66 insertions(+), 18 deletions(-) diff --git a/lib/Alchemy/Phrasea/Controller/Prod/BasketController.php b/lib/Alchemy/Phrasea/Controller/Prod/BasketController.php index 57c22d0dbe..843d1d476d 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/BasketController.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/BasketController.php @@ -203,6 +203,10 @@ private function getEntityManager() public function createBasket(Request $request) { + if (!$this->isCrsfValid($request, 'prodCreateBasket')) { + return $this->app->json(['success' => false , 'message' => 'invalid form'], 403); + } + $basket = new Basket(); $basket->setName($request->request->get('name', '')); @@ -271,6 +275,10 @@ public function removeBasketElement(Request $request, Basket $basket, $basket_el public function updateBasket(Request $request, Basket $basket) { + if (!$this->isCrsfValid($request, 'prodBasketRename')) { + return $this->app->json(['success' => false , 'message' => 'invalid form'], 403); + } + $success = false; try { @@ -305,16 +313,24 @@ public function updateBasket(Request $request, Basket $basket) public function displayUpdateForm(Basket $basket) { + $this->setSessionFormToken('prodBasketRename'); + return $this->render('prod/Baskets/Update.html.twig', ['basket' => $basket]); } public function displayReorderForm(Basket $basket) { + $this->setSessionFormToken('prodBasketReorder'); + return $this->render('prod/Baskets/Reorder.html.twig', ['basket' => $basket]); } public function reorder(Request $request, Basket $basket) { + if (!$this->isCrsfValid($request, 'prodBasketReorder')) { + return $this->app->json(['success' => false , 'message' => 'invalid form'], 403); + } + $ret = ['success' => false, 'message' => $this->app->trans('An error occured')]; try { $order = $request->request->get('element'); @@ -417,6 +433,8 @@ public function stealElements(Request $request, Basket $basket) public function displayCreateForm() { + $this->setSessionFormToken('prodCreateBasket'); + return $this->render('prod/Baskets/Create.html.twig'); } diff --git a/lib/Alchemy/Phrasea/Controller/Prod/PrinterController.php b/lib/Alchemy/Phrasea/Controller/Prod/PrinterController.php index edd01bba04..8a2f48e38f 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/PrinterController.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/PrinterController.php @@ -48,6 +48,8 @@ public function postPrinterAction(Request $request) $storyId = $r->singleStory()->getId(); } + $this->setSessionFormToken('prodPrint'); + return $this->render('prod/actions/printer_default.html.twig', [ 'printer' => $printer, 'message' => '', @@ -59,6 +61,10 @@ public function postPrinterAction(Request $request) public function printAction(Request $request) { + if (!$this->isCrsfValid($request, 'prodPrint')) { + $this->app->abort(403); + } + $printer = new RecordHelper\Printer($this->app, $request); $printer->setThumbnailName($request->request->get('thumbnail-chosen')); $printer->setPreviewName($request->request->get('preview-chosen')); diff --git a/lib/Alchemy/Phrasea/Controller/Prod/PushController.php b/lib/Alchemy/Phrasea/Controller/Prod/PushController.php index 9d9b063018..ca0d585655 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/PushController.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/PushController.php @@ -397,6 +397,10 @@ public function getListAction($list_id) */ public function addUserAction(Request $request) { + if (!$this->isCrsfValid($request, 'prodShareAddUser')) { + return $this->app->json(['success' => false , 'message' => 'invalid add user form'], 403); + } + $result = ['success' => false, 'message' => '', 'user' => null]; try { @@ -474,6 +478,8 @@ public function getAddUserFormAction(Request $request) { $params = ['callback' => $request->query->get('callback')]; + $this->setSessionFormToken('prodShareAddUser'); + return $this->render('prod/User/Add.html.twig', $params); } diff --git a/lib/Alchemy/Phrasea/Controller/Prod/StoryController.php b/lib/Alchemy/Phrasea/Controller/Prod/StoryController.php index dc5ec6bd5a..01c7528088 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/StoryController.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/StoryController.php @@ -45,6 +45,8 @@ public function displayCreateFormAction(Request $request) } } + $this->setSessionFormToken('prodCreateStory'); + return $this->render('prod/Story/Create.html.twig', [ 'isMultipleDataboxes' => count($databoxes) > 1 ? 1 : 0, 'isMultipleCollections' => count($collections) > 1 ? 1 : 0, @@ -56,6 +58,10 @@ public function displayCreateFormAction(Request $request) public function postCreateFormAction(Request $request) { + if (!$this->isCrsfValid($request, 'prodCreateStory')) { + return $this->app->json(['success' => false , 'message' => 'invalid form'], 403); + } + $collection = \collection::getByBaseId($this->app, $request->request->get('base_id')); if (!$this->getAclForUser()->has_right_on_base($collection->get_base_id(), \ACL::CANADDRECORD)) { @@ -209,6 +215,8 @@ public function displayReorderFormAction($sbas_id, $record_id) throw new \Exception('This is not a story'); } + $this->setSessionFormToken('prodStoryReorder'); + return $this->renderResponse('prod/Story/Reorder.html.twig', [ 'story' => $story, ]); @@ -216,6 +224,10 @@ public function displayReorderFormAction($sbas_id, $record_id) public function reorderAction(Request $request, $sbas_id, $record_id) { + if (!$this->isCrsfValid($request, 'prodStoryReorder')) { + return $this->app->json(['success' => false , 'message' => 'invalid form'], 403); + } + try { $story = new \record_adapter($this->app, $sbas_id, $record_id); $previousDescription = $story->getRecordDescriptionAsArray(); diff --git a/templates/web/prod/Baskets/Create.html.twig b/templates/web/prod/Baskets/Create.html.twig index a758c16e61..d8207647bf 100644 --- a/templates/web/prod/Baskets/Create.html.twig +++ b/templates/web/prod/Baskets/Create.html.twig @@ -9,4 +9,5 @@ {{ 'Ajouter ma selection courrante' | trans }} + diff --git a/templates/web/prod/Baskets/Reorder.html.twig b/templates/web/prod/Baskets/Reorder.html.twig index dbaa113496..f594af2925 100644 --- a/templates/web/prod/Baskets/Reorder.html.twig +++ b/templates/web/prod/Baskets/Reorder.html.twig @@ -2,22 +2,23 @@
- - - - -
-
- {% for element in basket.getElements() %} - - {% endfor %} - + + + + +
+
+ {% for element in basket.getElements() %} + + {% endfor %} + +
diff --git a/templates/web/prod/Baskets/Update.html.twig b/templates/web/prod/Baskets/Update.html.twig index c5b577252d..06f68fa601 100644 --- a/templates/web/prod/Baskets/Update.html.twig +++ b/templates/web/prod/Baskets/Update.html.twig @@ -5,5 +5,6 @@ +
diff --git a/templates/web/prod/Story/Create.html.twig b/templates/web/prod/Story/Create.html.twig index 6eeb5682bf..4c7344a2fe 100644 --- a/templates/web/prod/Story/Create.html.twig +++ b/templates/web/prod/Story/Create.html.twig @@ -43,5 +43,5 @@ {{ 'Ajouter ma selection courrante' | trans }} - + diff --git a/templates/web/prod/Story/Reorder.html.twig b/templates/web/prod/Story/Reorder.html.twig index 39d6a33b5c..0146fd41a9 100644 --- a/templates/web/prod/Story/Reorder.html.twig +++ b/templates/web/prod/Story/Reorder.html.twig @@ -18,6 +18,7 @@ {% endfor %} +
diff --git a/templates/web/prod/User/Add.html.twig b/templates/web/prod/User/Add.html.twig index 8661ac97da..4c92fc3656 100644 --- a/templates/web/prod/User/Add.html.twig +++ b/templates/web/prod/User/Add.html.twig @@ -35,6 +35,7 @@ +
diff --git a/templates/web/prod/actions/printer_default.html.twig b/templates/web/prod/actions/printer_default.html.twig index daf7e198d8..c880703e41 100644 --- a/templates/web/prod/actions/printer_default.html.twig +++ b/templates/web/prod/actions/printer_default.html.twig @@ -191,7 +191,8 @@ - + +