Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
15 / 15
CRAP
100.00% covered (success)
100.00%
1 / 1
AntiCSRF
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
15 / 15
23
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getTokenName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTokenName
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setToken
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getUserToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 verify
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 isSafeMethod
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 validate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isVerified
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setVerified
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 input
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 isEnabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 enable
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 disable
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php declare(strict_types=1);
2/*
3 * This file is part of Aplus Framework HTTP Library.
4 *
5 * (c) Natan Felles <natanfelles@gmail.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10namespace Framework\HTTP;
11
12use JetBrains\PhpStorm\Pure;
13use LogicException;
14
15/**
16 * Class AntiCSRF.
17 *
18 * @see https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern
19 * @see https://stackoverflow.com/q/6287903/6027968
20 * @see https://portswigger.net/web-security/csrf
21 * @see https://www.netsparker.com/blog/web-security/protecting-website-using-anti-csrf-token/
22 *
23 * @package http
24 */
25class AntiCSRF
26{
27    protected string $tokenName = 'csrf_token';
28    protected Request $request;
29    protected bool $verified = false;
30    protected bool $enabled = true;
31
32    /**
33     * AntiCSRF constructor.
34     *
35     * @param Request $request
36     */
37    public function __construct(Request $request)
38    {
39        if (\session_status() !== \PHP_SESSION_ACTIVE) {
40            throw new LogicException('Session must be active to use AntiCSRF class');
41        }
42        $this->request = $request;
43        if ($this->getToken() === null) {
44            $this->setToken();
45        }
46    }
47
48    /**
49     * Gets the anti-csrf token name.
50     *
51     * @return string
52     */
53    #[Pure]
54    public function getTokenName() : string
55    {
56        return $this->tokenName;
57    }
58
59    /**
60     * Sets the anti-csrf token name.
61     *
62     * @param string $tokenName
63     *
64     * @return static
65     */
66    public function setTokenName(string $tokenName) : static
67    {
68        $this->tokenName = \htmlspecialchars($tokenName, \ENT_QUOTES | \ENT_HTML5);
69        return $this;
70    }
71
72    /**
73     * Gets the anti-csrf token from the session.
74     *
75     * @return string|null
76     */
77    #[Pure]
78    public function getToken() : ?string
79    {
80        return $_SESSION['$']['csrf_token'] ?? null;
81    }
82
83    /**
84     * Sets the anti-csrf token into the session.
85     *
86     * @param string|null $token A custom anti-csrf token or null to generate one
87     *
88     * @return static
89     */
90    public function setToken(string $token = null) : static
91    {
92        $_SESSION['$']['csrf_token'] = $token ?? \base64_encode(\random_bytes(8));
93        return $this;
94    }
95
96    /**
97     * Gets the user token from the request input form.
98     *
99     * @return string|null
100     */
101    public function getUserToken() : ?string
102    {
103        return $this->request->getParsedBody($this->getTokenName());
104    }
105
106    /**
107     * Verifies the request input token, if the verification is enabled.
108     * The verification always succeed on HTTP GET, HEAD and OPTIONS methods.
109     * If verification is successful with other HTTP methods, a new token is
110     * generated.
111     *
112     * @return bool
113     */
114    public function verify() : bool
115    {
116        if ($this->isEnabled() === false) {
117            return true;
118        }
119        if ($this->isSafeMethod()) {
120            return true;
121        }
122        if ($this->getUserToken() === null) {
123            return false;
124        }
125        if ( ! $this->validate($this->getUserToken())) {
126            return false;
127        }
128        if ( ! $this->isVerified()) {
129            $this->setToken();
130            $this->setVerified();
131        }
132        return true;
133    }
134
135    /**
136     * Safe HTTP Request methods are: GET, HEAD and OPTIONS.
137     *
138     * @return bool
139     */
140    #[Pure]
141    public function isSafeMethod() : bool
142    {
143        return \in_array($this->request->getMethod(), [
144           Method::GET,
145           Method::HEAD,
146           Method::OPTIONS,
147        ], true);
148    }
149
150    /**
151     * Validates if a user token is equals the session token.
152     *
153     * This method can be used to validate tokens not received through forms.
154     * For example: Through a request header, JSON, etc.
155     *
156     * @param string $userToken
157     *
158     * @return bool
159     */
160    public function validate(string $userToken) : bool
161    {
162        return \hash_equals($_SESSION['$']['csrf_token'], $userToken);
163    }
164
165    #[Pure]
166    protected function isVerified() : bool
167    {
168        return $this->verified;
169    }
170
171    /**
172     * @param bool $status
173     *
174     * @return static
175     */
176    protected function setVerified(bool $status = true) : static
177    {
178        $this->verified = $status;
179        return $this;
180    }
181
182    /**
183     * Gets the HTML form hidden input if the verification is enabled.
184     *
185     * @return string
186     */
187    #[Pure]
188    public function input() : string
189    {
190        if ($this->isEnabled() === false) {
191            return '';
192        }
193        return '<input type="hidden" name="'
194            . $this->getTokenName() . '" value="'
195            . $this->getToken() . '">';
196    }
197
198    /**
199     * Tells if the verification is enabled.
200     *
201     * @see AntiCSRF::verify()
202     *
203     * @return bool
204     */
205    #[Pure]
206    public function isEnabled() : bool
207    {
208        return $this->enabled;
209    }
210
211    /**
212     * Enables the Anti CSRF verification.
213     *
214     * @see AntiCSRF::verify()
215     *
216     * @return static
217     */
218    public function enable() : static
219    {
220        $this->enabled = true;
221        return $this;
222    }
223
224    /**
225     * Disables the Anti CSRF verification.
226     *
227     * @see AntiCSRF::verify()
228     *
229     * @return static
230     */
231    public function disable() : static
232    {
233        $this->enabled = false;
234        return $this;
235    }
236}