1<?php
2/**
3 * XOOPS security handler
4 *
5 * You may not change or alter any portion of this comment or credits
6 * of supporting developers from this source code or any supporting source code
7 * which is considered copyrighted (c) material of the original comment or credit authors.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11 *
12 * @author    Kazumi Ono <onokazu@xoops.org>
13 * @author    Jan Pedersen <mithrandir@xoops.org>
14 * @author    John Neill <catzwolf@xoops.org>
15 * @copyright (c) 2000-2016 XOOPS Project (www.xoops.org)
16 * @license   GNU GPL 2 (http://www.gnu.org/licenses/gpl-2.0.html)
17 * @package   kernel
18 * @since     2.0.0
19 */
20
21defined('XOOPS_ROOT_PATH') || exit('Restricted access');
22
23/**
24 * Class XoopsSecurity
25 */
26class XoopsSecurity
27{
28    public $errors = array();
29
30    /**
31     * Check if there is a valid token in $_REQUEST[$name . '_REQUEST'] - can be expanded for more wide use, later (Mith)
32     *
33     * @param bool         $clearIfValid whether to clear the token after validation
34     * @param string|false $token        token to validate
35     * @param string       $name         name of session variable
36     *
37     * @return bool
38     */
39    public function check($clearIfValid = true, $token = false, $name = 'XOOPS_TOKEN')
40    {
41        return $this->validateToken($token, $clearIfValid, $name);
42    }
43
44    /**
45     * Create a token in the user's session
46     *
47     * @param int|string    $timeout time in seconds the token should be valid
48     * @param string $name    name of session variable
49     *
50     * @return string token value
51     */
52    public function createToken($timeout = 0, $name = 'XOOPS_TOKEN')
53    {
54        $this->garbageCollection($name);
55        if ($timeout == 0) {
56            $expire  = @ini_get('session.gc_maxlifetime');
57            $timeout = ($expire > 0) ? $expire : 900;
58        }
59        $token_id = md5(uniqid(mt_rand(), true));
60        // save token data on the server
61        if (!isset($_SESSION[$name . '_SESSION'])) {
62            $_SESSION[$name . '_SESSION'] = array();
63        }
64        $token_data = array(
65            'id'     => $token_id,
66            'expire' => time() + (int)$timeout);
67        $_SESSION[$name . '_SESSION'][] = $token_data;
68
69        return md5($token_id . $_SERVER['HTTP_USER_AGENT'] . XOOPS_DB_PREFIX);
70    }
71
72    /**
73     * Check if a token is valid. If no token is specified, $_REQUEST[$name . '_REQUEST'] is checked
74     *
75     * @param string|false $token        token to validate
76     * @param bool         $clearIfValid whether to clear the token value if valid
77     * @param string       $name         session name to validate
78     *
79     * @return bool
80     */
81    public function validateToken($token = false, $clearIfValid = true, $name = 'XOOPS_TOKEN')
82    {
83        global $xoopsLogger;
84        $token = ($token !== false) ? $token : (isset($_REQUEST[$name . '_REQUEST']) ? $_REQUEST[$name . '_REQUEST'] : '');
85        if (empty($token) || empty($_SESSION[$name . '_SESSION'])) {
86            $xoopsLogger->addExtra('Token Validation', 'No valid token found in request/session');
87
88            return false;
89        }
90        $validFound = false;
91        $token_data = &$_SESSION[$name . '_SESSION'];
92        foreach (array_keys($token_data) as $i) {
93            if ($token === md5($token_data[$i]['id'] . $_SERVER['HTTP_USER_AGENT'] . XOOPS_DB_PREFIX)) {
94                if ($this->filterToken($token_data[$i])) {
95                    if ($clearIfValid) {
96                        // token should be valid once, so clear it once validated
97                        unset($token_data[$i]);
98                    }
99                    $xoopsLogger->addExtra('Token Validation', 'Valid token found');
100                    $validFound = true;
101                } else {
102                    $str = 'Valid token expired';
103                    $this->setErrors($str);
104                    $xoopsLogger->addExtra('Token Validation', $str);
105                }
106            }
107        }
108        if (!$validFound && !isset($str)) {
109            $str = 'No valid token found';
110            $this->setErrors($str);
111            $xoopsLogger->addExtra('Token Validation', $str);
112        }
113        $this->garbageCollection($name);
114
115        return $validFound;
116    }
117
118    /**
119     * Clear all token values from user's session
120     *
121     * @param string $name session name
122     *
123     * @return void
124     */
125    public function clearTokens($name = 'XOOPS_TOKEN')
126    {
127        $_SESSION[$name . '_SESSION'] = array();
128    }
129
130    /**
131     * Check whether a token value is expired or not
132     *
133     * @param string $token token
134     *
135     * @return bool
136     */
137    public function filterToken($token)
138    {
139        return (!empty($token['expire']) && $token['expire'] >= time());
140    }
141
142    /**
143     * Perform garbage collection, clearing expired tokens
144     *
145     * @param string $name session name
146     *
147     * @return void
148     */
149    public function garbageCollection($name = 'XOOPS_TOKEN')
150    {
151        $sessionName = $name . '_SESSION';
152        if (!empty($_SESSION[$sessionName]) && is_array($_SESSION[$sessionName])) {
153            $_SESSION[$sessionName] = array_filter($_SESSION[$sessionName], array($this, 'filterToken'));
154        }
155    }
156
157    /**
158     * Check the user agent's HTTP REFERER against XOOPS_URL
159     *
160     * @param int $docheck 0 to not check the referer (used with XML-RPC), 1 to actively check it
161     *
162     * @return bool
163     */
164    public function checkReferer($docheck = 1)
165    {
166        $ref = xoops_getenv('HTTP_REFERER');
167        if ($docheck == 0) {
168            return true;
169        }
170        if ($ref == '') {
171            return false;
172        }
173        return !(strpos($ref, XOOPS_URL) !== 0);
174    }
175
176    /**
177     * Check superglobals for contamination
178     *
179     * @return void
180     **/
181    public function checkSuperglobals()
182    {
183        foreach (array(
184                     'GLOBALS',
185                     '_SESSION',
186                     'HTTP_SESSION_VARS',
187                     '_GET',
188                     'HTTP_GET_VARS',
189                     '_POST',
190                     'HTTP_POST_VARS',
191                     '_COOKIE',
192                     'HTTP_COOKIE_VARS',
193                     '_REQUEST',
194                     '_SERVER',
195                     'HTTP_SERVER_VARS',
196                     '_ENV',
197                     'HTTP_ENV_VARS',
198                     '_FILES',
199                     'HTTP_POST_FILES',
200                     'xoopsDB',
201                     'xoopsUser',
202                     'xoopsUserId',
203                     'xoopsUserGroups',
204                     'xoopsUserIsAdmin',
205                     'xoopsConfig',
206                     'xoopsOption',
207                     'xoopsModule',
208                     'xoopsModuleConfig',
209                     'xoopsRequestUri') as $bad_global) {
210            if (isset($_REQUEST[$bad_global])) {
211                header('Location: ' . XOOPS_URL . '/');
212                exit();
213            }
214        }
215    }
216
217    /**
218     * Check if visitor's IP address is banned
219     * Should be changed to return bool and let the action be up to the calling script
220     *
221     * @return void
222     */
223    public function checkBadips()
224    {
225        global $xoopsConfig;
226        if ($xoopsConfig['enable_badips'] == 1 && isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] != '') {
227            foreach ($xoopsConfig['bad_ips'] as $bi) {
228                if (!empty($bi) && preg_match('/' . $bi . '/', $_SERVER['REMOTE_ADDR'])) {
229                    exit();
230                }
231            }
232        }
233        unset($bi, $bad_ips, $xoopsConfig['badips']);
234    }
235
236    /**
237     * Get the HTML code for a XoopsFormHiddenToken object - used in forms that do not use XoopsForm elements
238     *
239     * @param string $name session token name
240     *
241     * @return string
242     */
243    public function getTokenHTML($name = 'XOOPS_TOKEN')
244    {
245        require_once XOOPS_ROOT_PATH . '/class/xoopsformloader.php';
246        $token = new XoopsFormHiddenToken($name);
247
248        return $token->render();
249    }
250
251    /**
252     * Add an error
253     *
254     * @param string $error message
255     *
256     * @return void
257     */
258    public function setErrors($error)
259    {
260        $this->errors[] = trim($error);
261    }
262
263    /**
264     * Get generated errors
265     *
266     * @param bool $ashtml Format using HTML?
267     *
268     * @return array|string Array of array messages OR HTML string
269     */
270    public function &getErrors($ashtml = false)
271    {
272        if (!$ashtml) {
273            return $this->errors;
274        } else {
275            $ret = '';
276            if (count($this->errors) > 0) {
277                foreach ($this->errors as $error) {
278                    $ret .= $error . '<br>';
279                }
280            }
281
282            return $ret;
283        }
284    }
285}
286