" . _t('DELETEPAGE_NOT_ORPHEANED') . "
\n"; $linkedFrom = $this->LoadAll("SELECT DISTINCT from_tag " . "FROM " . $this->config["table_prefix"] . "links " . "WHERE to_tag = '" . $this->GetPageTag() . "'"); @@ -84,6 +116,7 @@ $msg .= '" method="post" style="display: inline">' . "\n"; $msg .= str_replace("{tag}", $this->Link($this->tag), _t('DELETEPAGE_CONFIRM_WHEN_BACKLINKS')) . "\n"; $msg .= ''; + $msg .= 'tag}")) .'">'; $msg .= 'set(Wiki::class, $wiki); $containerBuilder->set(ParameterBagInterface::class, $containerBuilder->getParameterBag()); + $containerBuilder->set(CsrfTokenManager::class, new CsrfTokenManager()); $loader = new YamlFileLoader($containerBuilder, new FileLocator(__DIR__)); $loader->load('services.yaml'); diff --git a/includes/controllers/CsrfTokenController.php b/includes/controllers/CsrfTokenController.php new file mode 100644 index 000000000..97029f050 --- /dev/null +++ b/includes/controllers/CsrfTokenController.php @@ -0,0 +1,61 @@ +csrfTokenManager = $csrfTokenManager; + } + + /** + * check if token is present and valid in input + * + * @param string $name + * @param string $inputType "GET" or "POST" + * @param string $inputKey key in the input to use + * @return bool + * + * @throws TokenNotFoundException + * @throws Exception + */ + public function checkTocken(string $name, string $inputType, string $inputKey): bool + { + if (empty($name)) { + throw new Exception("parameter `\$name` should not be empty !"); + } + switch ($inputType) { + case 'GET': + $inputToken = filter_input(INPUT_GET, $inputKey, FILTER_SANITIZE_STRING); + break; + + case 'POST': + $inputToken = filter_input(INPUT_POST, $inputKey, FILTER_SANITIZE_STRING); + break; + + default: + throw new Exception("Unknown type for parameter `\$inputType` !"); + return false; + } + if (is_null($inputToken) || $inputToken === false) { + throw new TokenNotFoundException(_t('NO_CSRF_TOKEN_ERROR')); + } + $token = new CsrfToken($name, $inputToken); + $isValid = $this->csrfTokenManager->isTokenValid($token); + $this->csrfTokenManager->removeToken($name); + if (!$isValid) { + throw new TokenNotFoundException(_t('CSRF_TOKEN_FAIL_ERROR')); + } + return true; + } +} diff --git a/includes/services.yaml b/includes/services.yaml index a6837f874..739469668 100644 --- a/includes/services.yaml +++ b/includes/services.yaml @@ -8,6 +8,10 @@ services: Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface: synthetic: true + # Manually set inside the initCoreService method + Symfony\Component\Security\Csrf\CsrfTokenManager: + synthetic: true + # Manually set inside the initCoreService method # TODO remove this object when refactoring will be finished YesWiki\Wiki: @@ -15,3 +19,6 @@ services: YesWiki\Core\Service\: resource: 'services/*' + + YesWiki\Core\Controller\: + resource: 'controllers/*' diff --git a/includes/services/TemplateEngine.php b/includes/services/TemplateEngine.php index 5dce207d0..8cd92059c 100644 --- a/includes/services/TemplateEngine.php +++ b/includes/services/TemplateEngine.php @@ -2,7 +2,9 @@ namespace YesWiki\Core\Service; +use Exception; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; +use Symfony\Component\Security\Csrf\CsrfTokenManager; use YesWiki\Wiki; class TemplateNotFound extends \Exception @@ -15,11 +17,17 @@ class TemplateEngine protected $twigLoader; protected $twig; protected $assetsManager; - - public function __construct(Wiki $wiki, ParameterBagInterface $config, AssetsManager $assetsManager) - { + protected $csrfTokenManager; + + public function __construct( + Wiki $wiki, + ParameterBagInterface $config, + AssetsManager $assetsManager, + CsrfTokenManager $csrfTokenManager + ) { $this->wiki = $wiki; $this->assetsManager = $assetsManager; + $this->csrfTokenManager = $csrfTokenManager; // Default path (main namespace) is the root of the project. There are no templates // there, but it's needed to call relative path like render('tools/bazar/templates/...') $this->twigLoader = new \Twig\Loader\FilesystemLoader('./'); @@ -98,6 +106,23 @@ public function __construct(Wiki $wiki, ParameterBagInterface $config, AssetsMan $this->addTwigHelper('include_css', function ($file) { $this->assetsManager->AddCSSFile($file); }); + $this->addTwigHelper('crsfToken', function ($tokenId) { + if (is_string($tokenId)) { + return $this->csrfTokenManager->getToken($tokenId); + } elseif (is_array($tokenId)) { + if (!isset($tokenId['id'])) { + throw new Exception("When array, `\$tokenId` should contain `id` key !"); + } else { + if (isset($tokenId['refresh']) && $tokenId['refresh'] === true) { + return $this->csrfTokenManager->grefreshToken($tokenId['id']); + } else { + return $this->csrfTokenManager->getToken($tokenId['id']); + } + } + } else { + throw new Exception("`\$tokenId` should be a string or an array !"); + } + }); } private function addTwigHelper($name, $callback) diff --git a/lang/yeswiki_ca.php b/lang/yeswiki_ca.php index fa60bc186..943d770b5 100755 --- a/lang/yeswiki_ca.php +++ b/lang/yeswiki_ca.php @@ -269,6 +269,12 @@ // actions/wantedpages.php 'NO_PAGE_TO_CREATE' => 'No hi ha cap pàgina per crear', + // includes/controllers/CrsfController.php + 'NO_CSRF_TOKEN_ERROR' => 'Error de disseny del lloc: el formulari d\'enviament no contenia el testimoni '. + 'd\'identificació únic necessari per als mecanismes de seguretat interns.', + 'CSRF_TOKEN_FAIL_ERROR' => 'Pot ser que aquesta pàgina s\'hagi obert per segona vegada. '. + 'Si us plau, renoveu la sol·licitud des d\'aquesta finestra (el testimoni de seguretat intern no era bo).', + // setup/header.php 'OK' => 'D\'acord', 'FAIL' => 'Error', @@ -524,6 +530,7 @@ // 'DELETEPAGE_NOT_ORPHEANED' => 'Cette page n\'est pas orpheline.', // 'DELETEPAGE_NOT_OWNER' => 'Vous n\'êtes pas le propriétaire de cette page.', // 'DELETEPAGE_PAGES_WITH_LINKS_TO' => 'Pages ayant un lien vers {tag} :', + 'DELETEPAGE_NOT_DELETED' => 'La pàgina no s\'ha suprimit.', // handlers/edit // 'EDIT_ALERT_ALREADY_SAVED_BY_ANOTHER_USER' => 'ALERTE : '. diff --git a/lang/yeswiki_en.php b/lang/yeswiki_en.php index 7d75e188e..0e0ac3bb2 100755 --- a/lang/yeswiki_en.php +++ b/lang/yeswiki_en.php @@ -266,6 +266,12 @@ // actions/wantedpages.php 'NO_PAGE_TO_CREATE' => 'No page to create', + + // includes/controllers/CrsfController.php + 'NO_CSRF_TOKEN_ERROR' => 'Site design error: The submission form did not contain the unique '. + 'identification token needed for internal security mechanisms.', + 'CSRF_TOKEN_FAIL_ERROR' => 'This page may have been opened a second time. '. + 'Please renew the request from this window (the internal security token was not good).', // setup/header.php 'OK' => 'OK', @@ -485,6 +491,7 @@ 'DELETEPAGE_NOT_ORPHEANED' => 'This page is not orpheaned.', 'DELETEPAGE_NOT_OWNER' => 'You are not owner of this page.', 'DELETEPAGE_PAGES_WITH_LINKS_TO' => 'Pages with a link to {tag} :', + 'DELETEPAGE_NOT_DELETED' => 'Not deleted page.', // handlers/edit 'EDIT_ALERT_ALREADY_SAVED_BY_ANOTHER_USER' => 'ALERT : '. diff --git a/lang/yeswiki_es.php b/lang/yeswiki_es.php index ae569bcc8..1ba9ada25 100755 --- a/lang/yeswiki_es.php +++ b/lang/yeswiki_es.php @@ -271,6 +271,12 @@ // actions/wantedpages.php 'NO_PAGE_TO_CREATE' => 'Ninguna página para crear', + // includes/controllers/CrsfController.php + 'NO_CSRF_TOKEN_ERROR' => 'Error de diseño del sitio: el formulario de envío no contenía el token '. + 'de identificación único necesario para los mecanismos de seguridad internos.', + 'CSRF_TOKEN_FAIL_ERROR' => 'Es posible que esta página se haya abierto por segunda vez. '. + 'Renueve la solicitud desde esta ventana (el token de seguridad interno no era bueno).', + // setup/header.php 'OK' => 'OK', 'FAIL' => 'FRACASO', @@ -527,6 +533,7 @@ // 'DELETEPAGE_NOT_ORPHEANED' => 'Cette page n\'est pas orpheline.', // 'DELETEPAGE_NOT_OWNER' => 'Vous n\'êtes pas le propriétaire de cette page.', // 'DELETEPAGE_PAGES_WITH_LINKS_TO' => 'Pages ayant un lien vers {tag} :', + 'DELETEPAGE_NOT_DELETED' => 'Página no eliminada.', // handlers/edit // 'EDIT_ALERT_ALREADY_SAVED_BY_ANOTHER_USER' => 'ALERTE : '. diff --git a/lang/yeswiki_fr.php b/lang/yeswiki_fr.php index b324bcf89..6470580a9 100755 --- a/lang/yeswiki_fr.php +++ b/lang/yeswiki_fr.php @@ -267,6 +267,12 @@ // actions/wantedpages.php 'NO_PAGE_TO_CREATE' => 'Aucune page à créer', + // includes/controllers/CrsfController.php + 'NO_CSRF_TOKEN_ERROR' => 'Erreur de conception du site : Le formulaire de soumission ne contenait pas '. + 'le jeton d\'identification unique nécessaire aux mécanismes internes de sécurité.', + 'CSRF_TOKEN_FAIL_ERROR' => 'Cette page a peut-être été ouverte une seconde fois. '. + 'Veuillez renouveler la demande depuis cette fenêtre (le jeton interne de sécurité n\'était pas bon).', + // setup/header.php 'OK' => 'OK', 'FAIL' => 'ECHEC', @@ -522,6 +528,7 @@ 'DELETEPAGE_NOT_ORPHEANED' => 'Cette page n\'est pas orpheline.', 'DELETEPAGE_NOT_OWNER' => 'Vous n\'êtes pas le propriétaire de cette page.', 'DELETEPAGE_PAGES_WITH_LINKS_TO' => 'Pages ayant un lien vers {tag} :', + 'DELETEPAGE_NOT_DELETED' => 'Page non supprimée.', // handlers/edit 'EDIT_ALERT_ALREADY_SAVED_BY_ANOTHER_USER' => 'ALERTE : '. diff --git a/lang/yeswiki_nl.php b/lang/yeswiki_nl.php index 9d9ee3b77..00a4badc9 100755 --- a/lang/yeswiki_nl.php +++ b/lang/yeswiki_nl.php @@ -268,6 +268,12 @@ // actions/wantedpages.php 'NO_PAGE_TO_CREATE' => 'Geen enkele pagina aan te maken', + // includes/controllers/CrsfController.php + 'NO_CSRF_TOKEN_ERROR' => 'Fout bij het ontwerpen van de site: het indieningsformulier bevatte niet het unieke identificatietoken '. + 'dat nodig is voor interne beveiligingsmechanismen.', + 'CSRF_TOKEN_FAIL_ERROR' => 'Deze pagina is mogelijk een tweede keer geopend. '. + 'Verleng het verzoek vanuit dit venster (het interne beveiligingstoken was niet goed).', + // setup/header.php 'OK' => 'OK', 'FAIL' => 'MISLUKT', @@ -523,6 +529,7 @@ // 'DELETEPAGE_NOT_ORPHEANED' => 'Cette page n\'est pas orpheline.', // 'DELETEPAGE_NOT_OWNER' => 'Vous n\'êtes pas le propriétaire de cette page.', // 'DELETEPAGE_PAGES_WITH_LINKS_TO' => 'Pages ayant un lien vers {tag} :', + 'DELETEPAGE_NOT_DELETED' => 'Pagina niet verwijderd.', // handlers/edit // 'EDIT_ALERT_ALREADY_SAVED_BY_ANOTHER_USER' => 'ALERTE : '. diff --git a/lang/yeswiki_pt.php b/lang/yeswiki_pt.php index f2a78b821..82e41bc29 100755 --- a/lang/yeswiki_pt.php +++ b/lang/yeswiki_pt.php @@ -267,6 +267,12 @@ // actions/wantedpages.php 'NO_PAGE_TO_CREATE' => 'Nenhuma página para criar', + // includes/controllers/CrsfController.php + 'NO_CSRF_TOKEN_ERROR' => 'Erro de conceção do local: O formulário de submissão não continha o símbolo de identificação '. + 'único necessário para os mecanismos de segurança interna.', + 'CSRF_TOKEN_FAIL_ERROR' => 'Esta página pode ter sido aberta uma segunda vez. '. + 'Por favor, renove o pedido desta janela (o sinal de segurança interna não foi bom).', + // setup/header.php 'OK' => 'OK', 'FAIL' => 'FALHA', @@ -524,6 +530,7 @@ // 'DELETEPAGE_NOT_ORPHEANED' => 'Cette page n\'est pas orpheline.', // 'DELETEPAGE_NOT_OWNER' => 'Vous n\'êtes pas le propriétaire de cette page.', // 'DELETEPAGE_PAGES_WITH_LINKS_TO' => 'Pages ayant un lien vers {tag} :', + 'DELETEPAGE_NOT_DELETED' => 'Página não apagada.', // handlers/edit // 'EDIT_ALERT_ALREADY_SAVED_BY_ANOTHER_USER' => 'ALERTE : '. diff --git a/tools/login/actions/usersettings.php b/tools/login/actions/usersettings.php index 294e90ef4..8e6d69017 100644 --- a/tools/login/actions/usersettings.php +++ b/tools/login/actions/usersettings.php @@ -4,10 +4,18 @@ Software under AGPL Licence */ +use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException; +use Symfony\Component\Security\Csrf\CsrfTokenManager; +use YesWiki\Core\Controller\CsrfTokenController; + if (!defined('WIKINI_VERSION')) { die('accès direct interdit'); } +// get services +$csrfTokenManager = $this->services->get(CsrfTokenManager::class); +$csrfTokenController = $this->services->get(CsrfTokenController::class); + $userLoggedIn = false; $referrer=''; $isAdmin = $this->UserIsAdmin(); @@ -45,38 +53,50 @@ $this->Redirect($this->href()); } elseif ($adminIsActing || $userLoggedIn) { // Admin or user wants to manage the user if (substr($action, 0, 6) == 'update') { // Whoever it is tries to update the user - $OK = $this->user->setByAssociativeArray(array( - 'email' => isset($_POST['email']) ? $_POST['email'] : '', - 'motto' => isset($_POST['motto']) ? $_POST['motto'] : '', - 'revisioncount' => isset($_POST['revisioncount']) ? $_POST['revisioncount'] : '', - 'changescount' => isset($_POST['changescount']) ? $_POST['changescount'] : '', - 'doubleclickedit' => isset($_POST['doubleclickedit']) ? $_POST['doubleclickedit'] : '', - 'show_comments' => isset($_POST['show_comments']) ? $_POST['show_comments'] : '', - )); - if ($OK) { - $OK = $this->user->updateIntoDB('email, motto, revisioncount, changescount, doubleclickedit, show_comments'); - if ($userLoggedIn) { // In case it's the user trying to update oneself, need to reset the cooky - $this->user->logIn(); - } - // forward - $this->session->setMessage(_t('USER_PARAMETERS_SAVED').' !'); - if ($userLoggedIn) { // In case it's the usther trying to update oneself - $this->Redirect($this->href()); - } else { // That's the admin acting, we need to pass the user on - $this->Redirect($this->href('', '', 'user='.$_GET['user'].'&from='.$referrer, false)); + try { + $csrfTokenController->checkTocken('login\action\usersettings\updateuser', 'POST', 'crsf-token'); + + $OK = $this->user->setByAssociativeArray(array( + 'email' => isset($_POST['email']) ? $_POST['email'] : '', + 'motto' => isset($_POST['motto']) ? $_POST['motto'] : '', + 'revisioncount' => isset($_POST['revisioncount']) ? $_POST['revisioncount'] : '', + 'changescount' => isset($_POST['changescount']) ? $_POST['changescount'] : '', + 'doubleclickedit' => isset($_POST['doubleclickedit']) ? $_POST['doubleclickedit'] : '', + 'show_comments' => isset($_POST['show_comments']) ? $_POST['show_comments'] : '', + )); + if ($OK) { + $OK = $this->user->updateIntoDB('email, motto, revisioncount, changescount, doubleclickedit, show_comments'); + if ($userLoggedIn) { // In case it's the user trying to update oneself, need to reset the cooky + $this->user->logIn(); + } + // forward + $this->session->setMessage(_t('USER_PARAMETERS_SAVED').' !'); + if ($userLoggedIn) { // In case it's the usther trying to update oneself + $this->Redirect($this->href()); + } else { // That's the admin acting, we need to pass the user on + $this->Redirect($this->href('', '', 'user='.$_GET['user'].'&from='.$referrer, false)); + } + } else { // Unable to update + $this->session->setMessage($this->user->error); } - } else { // Unable to update - $this->session->setMessage($this->user->error); + } catch (TokenNotFoundException $th) { + $errorUpdate = _t('USERSETTINGS_EMAIL_NOT_CHANGED') .' '. $th->getMessage(); } } // End of update action if ($adminIsActing) { // Admin wants to manage the user if ($action == 'deleteByAdmin') { // Admin trying to delete user - $this->user->delete(); - // forward - $this->session->setMessage(_t('USER_DELETED').' !'); - $this->Redirect($this->href('', $referrer)); + try { + $csrfTokenController->checkTocken('login\action\usersettings\deleteByAdmin', 'POST', 'crsf-token'); + + $this->user->delete(); + // forward + $this->session->setMessage(_t('USER_DELETED').' !'); + $this->Redirect($this->href('', $referrer)); + } catch (TokenNotFoundException $th) { + $errorUpdate = _t('USERSETTINGS_USER_NOT_DELETED') .' '. $th->getMessage(); + } } // End of delete by admin action } elseif ($userLoggedIn) { // Admin isn't acting therefore that's an already logged in user @@ -84,13 +104,20 @@ if (!$this->user->checkPassword($_POST['oldpass'])) { // check password first $error = $this->user->error; } else { // user properly typed his old password in - $password = $_POST['password']; - if ($this->user->updatePassword($password)) { - $this->session->setMessage(_t('USER_PASSWORD_CHANGED').' !'); - $this->user->logIn(); - $this->Redirect($this->href()); - } else { // Something when wrong when updating the user in DB - $this->session->setMessage($this->user->error); + // check token + try { + $csrfTokenController->checkTocken('login\action\usersettings\changepass', 'POST', 'crsf-token'); + + $password = $_POST['password']; + if ($this->user->updatePassword($password)) { + $this->session->setMessage(_t('USER_PASSWORD_CHANGED').' !'); + $this->user->logIn(); + $this->Redirect($this->href()); + } else { // Something when wrong when updating the user in DB + $this->session->setMessage($this->user->error); + } + } catch (TokenNotFoundException $th) { + $error = _t('USERSETTINGS_PASSWORD_NOT_CHANGED') .' '. $th->getMessage(); } } } // End of changepass action @@ -102,6 +129,9 @@ if ($adminIsActing) { echo ' — '.$this->user->getProperty('name'); } ?> + + + href('', '', 'user='.$this->user->getProperty('name').'&from='.$referrer, false); @@ -129,6 +159,7 @@ ?>