From 1df48988a698e75b0577c2f324ac6b8d483ca801 Mon Sep 17 00:00:00 2001
From: Olivier PEREZ <olivier@olivierperez.fr>
Date: Sat, 10 Jan 2015 16:35:21 +0100
Subject: [PATCH] Use CSRF tokens on admin page

---
 admin/polls.php                               | 11 ++++--
 app/classes/Framadate/Security/Token.php      | 26 ++++++++++---
 .../Framadate/Services/SecurityService.php    | 38 +++++++++++++++++--
 tpl/admin/polls.tpl                           |  1 +
 4 files changed, 63 insertions(+), 13 deletions(-)

diff --git a/admin/polls.php b/admin/polls.php
index ac5780ad..bf91aac3 100644
--- a/admin/polls.php
+++ b/admin/polls.php
@@ -20,6 +20,7 @@
 use Framadate\Services\AdminPollService;
 use Framadate\Services\LogService;
 use Framadate\Services\PollService;
+use Framadate\Services\SecurityService;
 use Framadate\Services\SuperAdminService;
 use Framadate\Utils;
 
@@ -39,18 +40,19 @@ $logService = new LogService();
 $pollService = new PollService($connect, $logService);
 $adminPollService = new AdminPollService($connect, $pollService, $logService);
 $superAdminService = new SuperAdminService($connect);
+$securityService = new SecurityService();
 
 /* PAGE */
 /* ---- */
 
-if (!empty($_POST['delete_poll'])) {
-    $delete_id = filter_input(INPUT_POST, 'delete_poll', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^[a-z0-9]+$/']]);
+if (!empty($_POST['delete_poll']) && $securityService->checkCsrf('admin', $_POST['csrf'])) {
+    $delete_id = filter_input(INPUT_POST, 'delete_poll', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => POLL_REGEX]]);
     $poll_to_delete = $pollService->findById($delete_id);
 }
 
 // Traitement de la confirmation de suppression
-if (!empty($_POST['delete_confirm'])) {
-    $poll_id = filter_input(INPUT_POST, 'delete_confirm', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^[a-z0-9]+$/']]);
+if (!empty($_POST['delete_confirm']) && $securityService->checkCsrf('admin', $_POST['csrf'])) {
+    $poll_id = filter_input(INPUT_POST, 'delete_confirm', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => POLL_REGEX]]);
     $adminPollService->deleteEntirePoll($poll_id);
 }
 
@@ -60,5 +62,6 @@ $polls = $superAdminService->findAllPolls();
 $smarty->assign('polls', $polls);
 $smarty->assign('poll_to_delete', $poll_to_delete);
 $smarty->assign('log_file', is_readable('../' . LOG_FILE) ? LOG_FILE : null);
+$smarty->assign('crsf', $securityService->getToken('admin'));
 
 $smarty->display('admin/polls.tpl');
diff --git a/app/classes/Framadate/Security/Token.php b/app/classes/Framadate/Security/Token.php
index 2f06afca..7222dbfa 100644
--- a/app/classes/Framadate/Security/Token.php
+++ b/app/classes/Framadate/Security/Token.php
@@ -3,18 +3,32 @@ namespace Framadate\Security;
 
 class Token {
 
-    private $tokan_name;
     private $time;
     private $value;
 
-    function __construct($tokan_name, $time) {
-       $this->tokan_name = $tokan_name;
-       $this->time = $time;
-       $this->value = $this->generate();
+    function __construct() {
+        $this->time = time() + TOKEN_TIME;
+        $this->value = $this->generate();
     }
 
     private function generate() {
-        // TODO
+        return sha1(uniqid(mt_rand(), true));
+    }
+
+    public function getTime() {
+        return $this->time;
+    }
+
+    public function getValue() {
+        return $this->value;
+    }
+
+    public function isGone() {
+        return $this->time < time();
+    }
+
+    public function check($value) {
+        return $value === $this->value;
     }
 
 }
diff --git a/app/classes/Framadate/Services/SecurityService.php b/app/classes/Framadate/Services/SecurityService.php
index ea570e01..fc67a72b 100644
--- a/app/classes/Framadate/Services/SecurityService.php
+++ b/app/classes/Framadate/Services/SecurityService.php
@@ -8,12 +8,44 @@ class SecurityService {
     function __construct() {
     }
 
+    /**
+     * Get a CSRF token by name, or (re)create it.
+     *
+     * It creates a new token if :
+     * <ul>
+     *  <li>There no token with the given name in session</li>
+     *  <li>The token time is in past</li>
+     * </ul>
+     *
+     * @param $tokan_name string The name of the CSRF token
+     * @return Token The token
+     */
     function getToken($tokan_name) {
-        if (!isset($_SESSION['token']) || !isset($_SESSION['token'][$tokan_name])) {
-            $_SESSION['token'][$tokan_name] = new Token($tokan_name, 60*5);
+        if (!isset($_SESSION['tokens'])) {
+            $_SESSION['tokens'] = [];
+        }
+        if (!isset($_SESSION['tokens'][$tokan_name]) || $_SESSION['tokens'][$tokan_name]->isGone()) {
+            $_SESSION['tokens'][$tokan_name] = new Token();
+        }
+
+        return $_SESSION['tokens'][$tokan_name]->getValue();
+    }
+
+    /**
+     * Check if a given value is corresponding to the token in session.
+     *
+     * @param $tokan_name string Name of the token
+     * @param $csrf string Value to check
+     * @return bool true if the token is well checked
+     */
+    public function checkCsrf($tokan_name, $csrf) {
+        $checked = $_SESSION['tokens'][$tokan_name]->getValue() === $csrf;
+
+        if($checked) {
+            unset($_SESSION['tokens'][$tokan_name]);
         }
 
-        return $_SESSION['token'][$tokan_name]->getValue();
+        return $checked;
     }
 
 }
diff --git a/tpl/admin/polls.tpl b/tpl/admin/polls.tpl
index c594caaa..1758f13e 100644
--- a/tpl/admin/polls.tpl
+++ b/tpl/admin/polls.tpl
@@ -2,6 +2,7 @@
 
 {block 'admin_main'}
     <form action="" method="POST">
+        <input type="hidden" name="csrf" value="{$crsf}"/>
         {if $poll_to_delete}
             <div class="alert alert-warning text-center">
                 <h3>{_("Confirm removal of the poll ")}"{$poll_to_delete->id}"</h3>
-- 
GitLab