aboutsummaryrefslogtreecommitdiff
path: root/lib/classes/CSRFProtection.php
blob: 036aa932884ccf7dfb2accaccf5f2f2fb6e552dc (plain)
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
<?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();
            }

            self::$storage =& $_SESSION;
        }
        return self::$storage;
    }

    /**
     * This checks the request and throws an InvalidSecurityTokenException if
     * fails to verify its authenticity.
     *
     * @throws MethodNotAllowedException      The request has to be unsafe
     *                                        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()
    {
        $storage = &self::getStorage();

        // create a token, if there is none
        if (!isset($storage[self::TOKEN])) {
            $storage[self::TOKEN] = base64_encode(random_bytes(32));
        }

        return $storage[self::TOKEN];
    }

    /**
     * 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(),
        ]);

        return sprintf(
            '<input type="hidden" %s>',
            arrayToHtmlAttributes($attributes)
        );
    }

    /**
     * returns a random string token for XSRF prevention
     * the string is stored in the session
     *
     * @static
     * @return string
     */
    public static function sessionticket()
    {
        $storage = &self::getStorage();

        if (empty($storage['studipticket'])) {
            $storage['studipticket'] = md5(uniqid('studipticket', 1));
        }
        return $storage['studipticket'];
    }

    /**
     * checks the given string token against the one stored
     * in the session
     *
     * @static
     * @param string $studipticket
     * @return bool
     */
    public static function verifySessionticket($studipticket)
    {
        $storage = &self::getStorage();

        $check = (isset($storage['studipticket']) && $storage['studipticket'] === $studipticket);
        $storage['studipticket'] = null;
        return $check;
    }
}