Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
CsrfMiddleware
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 10
462
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSessionKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHeaderName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRequestKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getErrorHttpStatus
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRequestMode
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 handleBeforeHeader
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 handleBeforeBody
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 handleBefore
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 handleAfter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace PeServer\Core\Mvc\Middleware;
6
7use PeServer\Core\Collection\Arr;
8use PeServer\Core\Http\HttpResponse;
9use PeServer\Core\Http\HttpStatus;
10use PeServer\Core\Log\ILogger;
11use PeServer\Core\Mvc\Middleware\IMiddleware;
12use PeServer\Core\Mvc\Middleware\MiddlewareArgument;
13use PeServer\Core\Mvc\Middleware\MiddlewareResult;
14use PeServer\Core\Regex;
15use PeServer\Core\Web\WebSecurity;
16use PeServer\Core\Throws\NotImplementedException;
17
18/**
19 * CSRFミドルウェア。
20 */
21class CsrfMiddleware implements IMiddleware
22{
23    #region
24
25    protected const MODE_HEADER = 0;
26    protected const MODE_BODY = 1;
27
28    #endregion
29
30    #region variable
31
32    protected Regex $regex;
33
34    #endregion
35
36    public function __construct(
37        private WebSecurity $webSecurity,
38        private ILogger $logger,
39    ) {
40        $this->regex = new Regex();
41    }
42
43    #region function
44
45    /**
46     * CSRFとして有効なセッションキーを返す。
47     *
48     * @return non-empty-string
49     */
50    protected function getSessionKey(): string
51    {
52        return $this->webSecurity->getCsrfKind(WebSecurity::CSRF_KIND_SESSION_KEY);
53    }
54
55    /**
56     * CSRFとして有効なHTTPヘッダ名を返す。
57     *
58     * @return non-empty-string
59     */
60    protected function getHeaderName(): string
61    {
62        return $this->webSecurity->getCsrfKind(WebSecurity::CSRF_KIND_HEADER_NAME);
63    }
64
65    /**
66     * CSRFとして有効なリクエストキーを返す。
67     *
68     * @return non-empty-string
69     */
70    protected function getRequestKey(): string
71    {
72        return $this->webSecurity->getCsrfKind(WebSecurity::CSRF_KIND_REQUEST_NAME);
73    }
74
75    /**
76     * CSRFトークン不正時のHTTP応答ステータス。
77     *
78     * @return HttpStatus
79     */
80    protected function getErrorHttpStatus(): HttpStatus
81    {
82        return HttpStatus::MisdirectedRequest;
83    }
84
85    protected function getRequestMode(MiddlewareArgument $argument): int
86    {
87        if ($this->regex->isMatch($argument->requestPath->full, '/\Aajax\//')) {
88            return self::MODE_HEADER;
89        }
90
91        return self::MODE_BODY;
92    }
93
94    protected function handleBeforeHeader(MiddlewareArgument $argument): MiddlewareResult
95    {
96        if (!$argument->request->httpHeader->existsHeader($this->getHeaderName())) {
97            $this->logger->warn('要求CSRFトークンなし');
98            return MiddlewareResult::error($this->getErrorHttpStatus(), 'CSRF');
99        }
100
101        if ($argument->stores->session->tryGet($this->getSessionKey(), $sessionToken)) {
102            $values = $argument->request->httpHeader->getValues($this->getHeaderName());
103            if (Arr::getCount($values) === 1) {
104                if ($values[0] === $sessionToken) {
105                    return MiddlewareResult::none();
106                }
107                $this->logger->error('CSRFトークン不一致');
108            } else {
109                $this->logger->error('CSRFトークン数異常');
110            }
111        } else {
112            $this->logger->warn('セッションCSRFトークンなし');
113        }
114
115
116        return MiddlewareResult::error($this->getErrorHttpStatus(), 'CSRF');
117    }
118
119    protected function handleBeforeBody(MiddlewareArgument $argument): MiddlewareResult
120    {
121        $result = $argument->request->exists($this->getRequestKey());
122        if (!$result->exists) {
123            $this->logger->warn('要求CSRFトークンなし');
124            return MiddlewareResult::error($this->getErrorHttpStatus(), 'CSRF');
125        }
126
127        $requestToken = $argument->request->getValue($this->getRequestKey());
128        if ($argument->stores->session->tryGet($this->getSessionKey(), $sessionToken)) {
129            if ($requestToken === $sessionToken) {
130                return MiddlewareResult::none();
131            }
132            $this->logger->error('CSRFトークン不一致');
133        } else {
134            $this->logger->warn('セッションCSRFトークンなし');
135        }
136
137        return MiddlewareResult::error($this->getErrorHttpStatus(), 'CSRF');
138    }
139
140    #endregion
141
142    #region IMiddleware
143
144    public function handleBefore(MiddlewareArgument $argument): MiddlewareResult
145    {
146        return match ($this->getRequestMode($argument)) {
147            self::MODE_HEADER => $this->handleBeforeHeader($argument),
148            self::MODE_BODY => $this->handleBeforeBody($argument),
149            default => throw new NotImplementedException(),
150        };
151    }
152
153    final public function handleAfter(MiddlewareArgument $argument, HttpResponse $response): MiddlewareResult
154    {
155        return MiddlewareResult::none();
156    }
157
158    #endregion
159}