Completed
Push — master ( fdbf7c...2644c6 )
by Sebastian
04:13
created

CsrfGuard::getHiddenInput()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Linna Cross-site Request Forgery Guard
5
 *
6
 * @author Sebastian Rapetti <[email protected]>
7
 * @copyright (c) 2018, Sebastian Rapetti
8
 * @license http://opensource.org/licenses/MIT MIT License
9
 */
10
declare(strict_types=1);
11
12
namespace Linna;
13
14
use RuntimeException;
15
16
/**
17
 * Cross-site Request Forgery Guard
18
 */
19
class CsrfGuard
20
{
21
    /**
22
     * @var array Php session data from superglobal.
23
     */
24
    private $session;
25
26
    /**
27
     * @var int Max number of tokens stored in session.
28
     */
29
    private $maxStorage;
30
31
    /**
32
     * @var int Rapresent the lenght of the token in bytes.
33
     */
34
    private $tokenStrength;
35
36
    /**
37
     * Class constructor.
38
     *
39
     * @param int $maxStorage    Max number of tokens stored in session, work as
40
     *                           FIFO data structure, when maximun capacity is
41
     *                           reached, oldest token be dequeued from storage.
42
     * @param int $tokenStrength Rapresent the lenght of the token in bytes.
43
     */
44 23
    public function __construct(int $maxStorage, int $tokenStrength)
45
    {
46 23
        if (session_status() === 1) {
47 1
            throw new RuntimeException(__CLASS__.': Session must be started before create '.__CLASS__.' instance.');
48
        }
49
50
        //if csrf array doesn't exist inside session, initialize it.
51
        //for code shortness: Null coalescing operator
52
        //http://php.net/manual/en/migration70.new-features.php
53 22
        $_SESSION['CSRF'] = $_SESSION['CSRF'] ?? [];
54
55 22
        $this->session = &$_SESSION;
56 22
        $this->maxStorage = $maxStorage;
57 22
        $this->tokenStrength = $tokenStrength;
58 22
    }
59
60
    /**
61
     * Limit number of token stored in session.
62
     *
63
     * @param array $array
64
     */
65 21
    private function dequeue(array &$array): void
66
    {
67 21
        $size = count($array);
68
69 21
        while ($size > $this->maxStorage) {
70 14
            array_shift($array);
71 14
            $size--;
72
        }
73 21
    }
74
75
    /**
76
     * Return csrf token as array.
77
     *
78
     * @return array
79
     */
80 18
    public function getToken(): array
81
    {
82 18
        $token = $this->generateToken();
83
84 18
        $name = $token['name'];
85
86 18
        $this->session['CSRF'][$name] = $token;
87
88
        //storage cleaning!
89
        //warning!! if you get in a page more token of maximun storage,
90
        //will there a leak of token, the firsts generated
91
        //in future I think throw and exception.
92 18
        $this->dequeue($this->session['CSRF']);
93
94 18
        return $token;
95
    }
96
97
    /**
98
     * Return timed csrf token as array.
99
     *
100
     * @param int $ttl Time to live for the token.
101
     *
102
     * @return array
103
     */
104 3
    public function getTimedToken(int $ttl): array
105
    {
106 3
        $token = $this->generateToken();
107 3
        $token['time'] = time() + $ttl;
108
109 3
        $name = $token['name'];
110
111 3
        $this->session['CSRF'][$name] = $token;
112
113 3
        $this->dequeue($this->session['CSRF']);
114
115 3
        return $token;
116
    }
117
118
    /**
119
     * Generate a random token.
120
     *
121
     * @return array
122
     */
123 21
    private function generateToken(): array
124
    {
125 21
        $name = 'csrf_'.bin2hex(random_bytes(8));
126 21
        $value = bin2hex(random_bytes($this->tokenStrength));
127
128 21
        return ['name' => $name, 'value' => $value];
129
    }
130
131
    /**
132
     * Validate a csrf token or a csrf timed token.
133
     *
134
     * @param array $requestData From request or from superglobal variables $_POST,
135
     *                           $_GET, $_REQUEST and $_COOKIE.
136
     *
137
     * @return bool
138 1
     */
139
    public function validate(array $requestData): bool
140 1
    {
141
        //apply matchToken method elements of passed data,
142 1
        //using this instead of forach for code shortness.
143
        $array = array_filter($requestData, array($this, 'doChecks'), ARRAY_FILTER_USE_BOTH);
144
145
        return (bool) count($array);
146
    }
147
148
    /**
149
     * Tests for valid token.
150
     *
151
     * @param string $value
152
     * @param string $key
153 4
     *
154
     * @return bool
155
     */
156
    private function doChecks(string $value, string $key): bool
157 4
    {
158
        $tokens = &$this->session['CSRF'];
159 4
160
        return $this->tokenIsValid($tokens, $value, $key) &&
161
               $this->tokenIsExiperd($tokens, $key)  &&
162
               $this->deleteToken($tokens, $key);
163
    }
164
165
    /**
166
     * Delete token after validation.
167
     *
168
     * @param array $tokens
169
     * @param string $key
170 4
     * @return bool
171
     */
172 4
    private function deleteToken(array &$tokens, string &$key): bool
173
    {
174 4
        unset($tokens[$key]);
175 4
176 4
        return true;
177
    }
178
179
    /**
180
     * Check if token is valid
181
     *
182
     * @param array $tokens
183
     * @param string $value
184
     * @param string $key
185
     *
186 3
     * @return bool
187
     */
188 3
    private function tokenIsValid(array &$tokens, string &$value, string &$key): bool
189
    {
190 3
        //if token exist
191
        if (!isset($tokens[$key])) {
192
            return false;
193
        }
194
195
        //if token has valid value
196
        if (!hash_equals($tokens[$key]['value'], $value)) {
197
            return false;
198
        }
199
200
        return true;
201
    }
202 4
203
    /**
204
     * Check if timed token is expired.
205 4
     *
206 2
     * @param array $tokens
207
     * @param string $key
208
     *
209
     * @return bool
210 4
     */
211 1
    private function tokenIsExiperd(array &$tokens, string &$key): bool
212
    {
213
        //if timed and if time is valid
214 4
        if (isset($tokens[$key]['time']) && $tokens[$key]['time'] < time()) {
215
            return false;
216
        }
217
218
        return true;
219
    }
220
}
221