Issues (35)

src/Elements/Csrf.php (1 issue)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Enjoys\Forms\Elements;
6
7
use Enjoys\Forms\Exception\CsrfAttackDetected;
8
use Enjoys\Forms\Exception\ExceptionRule;
9
use Enjoys\Forms\Form;
10
use Enjoys\Forms\Rules;
11
use Enjoys\Session\Session;
12
13
/**
14
 * Включает защиту от CSRF.
15
 * Сross Site Request Forgery — «Подделка межсайтовых запросов», также известен как XSRF
16
 */
17
class Csrf extends Hidden
18
{
19
    /**
20
     * @throws ExceptionRule
21
     * @throws \Exception
22
     */
23 97
    public function __construct(private Session $session)
24
    {
25 97
        $csrfSecret = $this->getCsrfSecret();
26 96
        $token = $this->getCsrfToken($csrfSecret);
27
28
29 96
        parent::__construct(Form::_TOKEN_CSRF_, $token);
30
31 96
        $this->addRule(
32 96
            Rules::CALLBACK,
33 96
            'CSRF Attack detected',
34 96
            function (string $key) {
35
                /** @psalm-suppress  PossiblyNullArgument, MixedArgument */
36 3
                if (password_verify($key, $this->getRequest()->getParsedBody()[Form::_TOKEN_CSRF_] ?? '')) {
37 2
                    return true;
38
                }
39 1
                throw new CsrfAttackDetected('CSRF Token is invalid');
40 96
            },
41 96
            $csrfSecret
42 96
        );
43
    }
44
45 93
    public function prepare(): bool
46
    {
47 93
        if (!in_array($this->getForm()?->getMethod(), ['POST', 'PUT', 'DELETE', 'PATCH'], true)) {
48
            //удаляем элемент, если был заранее создан
49
            //$this->getForm()->removeElement($this->getForm()->getElement(\Enjoys\Forms\Form::_TOKEN_CSRF_));
50 18
            $this->getForm()?->removeElement($this);
51
52
            //возвращаем true, чтобы не добавлять элемент.
53 18
            return true;
54
        }
55 81
        $this->unsetForm();
56 81
        return false;
57
    }
58
59
    /**
60
     * @throws \Exception
61
     */
62 97
    private function getCsrfSecret(): string
63
    {
64 97
        $secret = (string)$this->session->get('csrf_secret');
65
66 96
        if (empty($secret)) {
67 81
            $secret = $this->generateSecret();
68
        }
69
70 96
        return $secret;
71
    }
72
73
74 96
    public function getCsrfToken(string $secret): string
75
    {
76 96
        return password_hash($secret, PASSWORD_DEFAULT);
0 ignored issues
show
Bug Best Practice introduced by
The expression return password_hash($se...ments\PASSWORD_DEFAULT) could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
77
    }
78
79
    /**
80
     * @return string
81
     * @throws \Exception
82
     */
83 81
    private function generateSecret(): string
84
    {
85 81
        $secret = base64_encode(random_bytes(32));
86 81
        $this->session->set([
87 81
            'csrf_secret' => $secret
88 81
        ]);
89 81
        return $secret;
90
    }
91
}
92