1 <?php
  2 
  3 /**
  4  * Class for generating and validating csrf tokens.
  5  *
  6  * @author gharlan
  7  *
  8  * @package redaxo\core
  9  */
 10 class rex_csrf_token
 11 {
 12     use rex_factory_trait;
 13 
 14     const PARAM = '_csrf_token';
 15 
 16     private $id;
 17 
 18     private function __construct($tokenId)
 19     {
 20         $this->id = $tokenId;
 21     }
 22 
 23     /**
 24      * @param string $tokenId
 25      *
 26      * @return static
 27      */
 28     public static function factory($tokenId)
 29     {
 30         $class = static::getFactoryClass();
 31 
 32         return new $class($tokenId);
 33     }
 34 
 35     /**
 36      * @return string
 37      */
 38     public function getId()
 39     {
 40         return $this->id;
 41     }
 42 
 43     /**
 44      * @return string
 45      */
 46     public function getValue()
 47     {
 48         $tokens = self::getTokens();
 49 
 50         if (isset($tokens[$this->id])) {
 51             return $tokens[$this->id];
 52         }
 53 
 54         $token = self::generateToken();
 55         $tokens[$this->id] = $token;
 56         rex_set_session(self::getSessionKey(), $tokens);
 57 
 58         return $token;
 59     }
 60 
 61     /**
 62      * @return string
 63      */
 64     public function getHiddenField()
 65     {
 66         return sprintf('<input type="hidden" name="%s" value="%s"/>', self::PARAM, $this->getValue());
 67     }
 68 
 69     /**
 70      * Returns an array containing the `_csrf_token` param.
 71      *
 72      * @return array
 73      */
 74     public function getUrlParams()
 75     {
 76         return [self::PARAM => $this->getValue()];
 77     }
 78 
 79     /**
 80      * @return bool
 81      */
 82     public function isValid()
 83     {
 84         $tokens = self::getTokens();
 85 
 86         if (!isset($tokens[$this->id])) {
 87             return false;
 88         }
 89 
 90         $token = rex_request(self::PARAM, 'string');
 91 
 92         return hash_equals($tokens[$this->id], $token);
 93     }
 94 
 95     public function remove()
 96     {
 97         $tokens = self::getTokens();
 98 
 99         if (!isset($tokens[$this->id])) {
100             return;
101         }
102 
103         unset($tokens[$this->id]);
104 
105         rex_set_session(self::getSessionKey(), $tokens);
106     }
107 
108     public static function removeAll()
109     {
110         rex_login::startSession();
111 
112         rex_unset_session(self::getBaseSessionKey());
113         rex_unset_session(self::getBaseSessionKey().'_https');
114     }
115 
116     private static function getTokens()
117     {
118         rex_login::startSession();
119 
120         return rex_session(self::getSessionKey(), 'array');
121     }
122 
123     private static function getSessionKey()
124     {
125         // use separate tokens for http/https
126         // http://symfony.com/blog/cve-2017-16653-csrf-protection-does-not-use-different-tokens-for-http-and-https
127         $suffix = rex_request::isHttps() ? '_https' : '';
128 
129         return self::getBaseSessionKey().$suffix;
130     }
131 
132     private static function getBaseSessionKey()
133     {
134         return 'csrf_tokens_'.rex::getEnvironment();
135     }
136 
137     private static function generateToken()
138     {
139         $bytes = random_bytes(32);
140 
141         return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
142     }
143 }
144