Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 37 |
|
0.00% |
0 / 10 |
CRAP | |
0.00% |
0 / 1 |
CsrfMiddleware | |
0.00% |
0 / 37 |
|
0.00% |
0 / 10 |
462 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSessionKey | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getHeaderName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRequestKey | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getErrorHttpStatus | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRequestMode | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
handleBeforeHeader | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
handleBeforeBody | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
handleBefore | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
handleAfter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace PeServer\Core\Mvc\Middleware; |
6 | |
7 | use PeServer\Core\Collection\Arr; |
8 | use PeServer\Core\Http\HttpResponse; |
9 | use PeServer\Core\Http\HttpStatus; |
10 | use PeServer\Core\Log\ILogger; |
11 | use PeServer\Core\Mvc\Middleware\IMiddleware; |
12 | use PeServer\Core\Mvc\Middleware\MiddlewareArgument; |
13 | use PeServer\Core\Mvc\Middleware\MiddlewareResult; |
14 | use PeServer\Core\Regex; |
15 | use PeServer\Core\Web\WebSecurity; |
16 | use PeServer\Core\Throws\NotImplementedException; |
17 | |
18 | /** |
19 | * CSRFミドルウェア。 |
20 | */ |
21 | class 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 | } |