aboutsummaryrefslogtreecommitdiff
path: root/lib/classes/CSRFProtection.php
blob: 4a995927ef6bf9af3ff7dd3c3c23067e9608bba7 (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
<?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';


    /**
     * This checks the request and throws an InvalidSecurityTokenException if
     * fails to verify its authenticity.
     *
     * @throws MethodNotAllowed               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
     */
    public 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()
    {
        // w/o a session, throw an exception
        if (session_id() === '') {
            throw new SessionRequiredException();
        }

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

        return $_SESSION[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
     *
     * @return string  the HTML snippet containing the input element
     */
    public static function tokenTag()
    {
        return sprintf(
            '<input type="hidden" name="%s" value="%s">',
            self::TOKEN,
            self::token()
        );
    }
}