Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
# Lifter010: DONE
/**
* CSRFProtection.php - protect from request forgery
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* @author mlunzena@uos.de
* @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
* @category Stud.IP
*/
/**
* To protect Stud.IP from forged request from other sites a security token is
* generated and stored in the session and all forms (or rather POST request)
* have to contain that token which is then compared on the server side to
* verify the authenticity of the request. GET request are not checked as these
* are assumed to be idempotent anyway.
*
* If a forgery is detected, an InvalidSecurityTokenException is thrown and a
* log entry is recorded in the error log.
*
* The (form or request) parameter is named "security token". If you are
* authoring an HTML form, you have to include this as an
* input[@type=hidden] element. This is easily done by calling:
*
* \code
* echo CSRFProtection::tokenTag();
* \endcode
*
* Checking the token is implicitly done when calling #page_open in file
* lib/phplib/page4.inc
*/
class CSRFProtection
{
/**
* The name of the parameter.
*/
const TOKEN = 'security_token';
const AJAX_TOKEN = 'HTTP_X_CSRF_TOKEN';
protected static $storage = null;
/**
* Set a storage to use.
*
* @param $storage
*/
public static function setStorage(&$storage): void
{
self::$storage = &$storage;
}
/**
* Returns a reference to the used storage.
*
* @return array|null
*/
public static function &getStorage()
{
if (!isset(self::$storage)) {
// w/o a session, throw an exception since we cannot use it
if (session_id() === '') {
throw new SessionRequiredException();
}
/**
* This checks the request and throws an InvalidSecurityTokenException if
* fails to verify its authenticity.
*
* @throws MethodNotAllowedException The request has to be unsafe
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
* in terms of RFC 2616.
* @throws InvalidSecurityTokenException The request is invalid as the
* security token does not match.
*/
public static function verifyUnsafeRequest()
{
if (self::isSafeRequestMethod()) {
throw new MethodNotAllowedException();
}
if (!self::checkSecurityToken()) {
throw new InvalidSecurityTokenException();
}
}
/**
* @return boolean true if the request method is either GET or HEAD
*/
private static function isSafeRequestMethod()
{
return in_array(Request::method(), ['GET', 'HEAD']);
}
/**
* This checks the request and throws an InvalidSecurityTokenException if
* fails to verify its authenticity.
*
* @throws InvalidSecurityTokenException request is invalid
*/
public static function verifySecurityToken()
{
if (!self::verifyRequest()) {
throw new InvalidSecurityTokenException();
}
}
/**
* This checks the request and returns either true or false. It is
* implicitly called by CSRFProtection::verifySecurityToken() and
* it should never be needed to call this.
*
* @returns boolean returns true if the request is valid
*/
private static function verifyRequest()
{
return Request::isGet() || self::checkSecurityToken();
}
/**
* Verifies the equality of the request parameter "security_token" and
* the token stored in the session.
*
* @return boolean true if equal
*/
private static function checkSecurityToken()
{
return self::token() === ($_POST[self::TOKEN] ?? $_SERVER[self::AJAX_TOKEN] ?? null);
}
/**
* Returns the token stored in the session generating it first
* if required.
*
* @return string a base64 encoded string of 32 random bytes
* @throws SessionRequiredException there is no session to store the token in
*/
public static function token()
{
// create a token, if there is none
if (!isset($storage[self::TOKEN])) {
$storage[self::TOKEN] = base64_encode(random_bytes(32));
}
/**
* Returns a snippet of HTML containing an input[@type=hidden] element
* like this:
*
* \code
* <input type="hidden" name="security_token" value="012345678901234567890123456789==">
* \endcode
*
* @param array $attributes Additional attributes to be added to the input
* @return string the HTML snippet containing the input element
*/
public static function tokenTag(array $attributes = [])
$attributes = array_merge($attributes, [
'name' => self::TOKEN,
'value' => self::token(),
]);
'<input type="hidden" %s>',
arrayToHtmlAttributes($attributes)