This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
1 | <?php |
||||
2 | |||||
3 | declare(strict_types=1); |
||||
4 | |||||
5 | namespace SimpleSAML\Module\cdc; |
||||
6 | |||||
7 | use SimpleSAML\Assert\Assert; |
||||
8 | use SimpleSAML\Configuration; |
||||
9 | use SimpleSAML\Error; |
||||
10 | use SimpleSAML\Logger; |
||||
11 | use SimpleSAML\SAML2\Exception\ProtocolViolationException; |
||||
12 | use SimpleSAML\Utils; |
||||
13 | |||||
14 | /** |
||||
15 | * CDC server class. |
||||
16 | * |
||||
17 | * @package SimpleSAMLphp |
||||
18 | */ |
||||
19 | |||||
20 | class Server |
||||
21 | { |
||||
22 | /** |
||||
23 | * The domain. |
||||
24 | * |
||||
25 | * @var string |
||||
26 | */ |
||||
27 | private string $domain; |
||||
28 | |||||
29 | /** |
||||
30 | * The URL to the server. |
||||
31 | * |
||||
32 | * @var string |
||||
33 | */ |
||||
34 | private string $server; |
||||
35 | |||||
36 | /** |
||||
37 | * Our shared key. |
||||
38 | * |
||||
39 | * @var string |
||||
40 | */ |
||||
41 | private string $key; |
||||
42 | |||||
43 | |||||
44 | /** |
||||
45 | * The lifetime of our cookie, in seconds. |
||||
46 | * |
||||
47 | * If this is 0, the cookie will expire when the browser is closed. |
||||
48 | * |
||||
49 | * @var int |
||||
50 | */ |
||||
51 | private int $cookieLifetime; |
||||
52 | |||||
53 | |||||
54 | /** |
||||
55 | * Initialize a CDC server. |
||||
56 | * |
||||
57 | * @param string $domain The domain we are a server for. |
||||
58 | * @throws \SimpleSAML\Error\Exception |
||||
59 | */ |
||||
60 | public function __construct(string $domain) |
||||
61 | { |
||||
62 | $cdcConfig = Configuration::getConfig('module_cdc.php'); |
||||
63 | $config = $cdcConfig->getOptionalConfigItem($domain, null); |
||||
64 | |||||
65 | if ($config === null) { |
||||
66 | throw new Error\Exception('Unknown CDC domain: ' . var_export($domain, true)); |
||||
67 | } |
||||
68 | |||||
69 | $this->domain = $domain; |
||||
70 | $this->server = $config->getString('server'); |
||||
71 | $this->key = $config->getString('key'); |
||||
72 | $this->cookieLifetime = $config->getOptionalInteger('cookie.lifetime', 0); |
||||
73 | |||||
74 | if ($this->key === 'ExampleSharedKey') { |
||||
75 | throw new Error\Exception( |
||||
76 | 'Key for CDC domain ' . var_export($domain, true) . ' not changed from default.', |
||||
77 | ); |
||||
78 | } |
||||
79 | } |
||||
80 | |||||
81 | |||||
82 | /** |
||||
83 | * Send a request to this CDC server. |
||||
84 | * |
||||
85 | * @param array $request The CDC request. |
||||
86 | */ |
||||
87 | public function sendRequest(array $request): void |
||||
88 | { |
||||
89 | Assert::keyExists($request, 'return'); |
||||
90 | Assert::keyExists($request, 'op'); |
||||
91 | |||||
92 | $request['domain'] = $this->domain; |
||||
93 | $this->send($this->server, 'CDCRequest', $request); |
||||
94 | } |
||||
95 | |||||
96 | |||||
97 | /** |
||||
98 | * Parse and validate response received from a CDC server. |
||||
99 | * |
||||
100 | * @return array|null The response, or NULL if no response is received. |
||||
101 | * @throws \SimpleSAML\Error\Exception |
||||
102 | */ |
||||
103 | public function getResponse(): ?array |
||||
104 | { |
||||
105 | $response = self::get('CDCResponse'); |
||||
106 | if ($response === null) { |
||||
107 | return null; |
||||
108 | } |
||||
109 | |||||
110 | if ($response['domain'] !== $this->domain) { |
||||
111 | throw new Error\Exception('Response received from wrong domain.'); |
||||
112 | } |
||||
113 | |||||
114 | $this->validate('CDCResponse'); |
||||
115 | |||||
116 | return $response; |
||||
117 | } |
||||
118 | |||||
119 | |||||
120 | /** |
||||
121 | * Parse and process a CDC request. |
||||
122 | * @throws \SimpleSAML\Error\BadRequest |
||||
123 | */ |
||||
124 | public static function processRequest(): void |
||||
125 | { |
||||
126 | $request = self::get('CDCRequest'); |
||||
127 | if ($request === null) { |
||||
128 | throw new Error\BadRequest('Missing "CDCRequest" parameter.'); |
||||
129 | } |
||||
130 | |||||
131 | $domain = $request['domain']; |
||||
132 | $server = new Server($domain); |
||||
133 | |||||
134 | $server->validate('CDCRequest'); |
||||
135 | $server->handleRequest($request); |
||||
136 | } |
||||
137 | |||||
138 | |||||
139 | /** |
||||
140 | * Handle a parsed CDC requst. |
||||
141 | * |
||||
142 | * @param array $request |
||||
143 | * @throws \SimpleSAML\Error\Exception |
||||
144 | */ |
||||
145 | private function handleRequest(array $request): void |
||||
146 | { |
||||
147 | if (!isset($request['op'])) { |
||||
148 | throw new Error\BadRequest('Missing "op" in CDC request.'); |
||||
149 | } |
||||
150 | $op = (string) $request['op']; |
||||
151 | |||||
152 | Logger::info('Received CDC request with "op": ' . var_export($op, true)); |
||||
153 | |||||
154 | if (!isset($request['return'])) { |
||||
155 | throw new Error\BadRequest('Missing "return" in CDC request.'); |
||||
156 | } |
||||
157 | $return = (string) $request['return']; |
||||
158 | |||||
159 | switch ($op) { |
||||
160 | case 'append': |
||||
161 | $response = $this->handleAppend($request); |
||||
162 | break; |
||||
163 | case 'delete': |
||||
164 | $response = $this->handleDelete($request); |
||||
165 | break; |
||||
166 | case 'read': |
||||
167 | $response = $this->handleRead($request); |
||||
168 | break; |
||||
169 | default: |
||||
170 | $response = 'unknown-op'; |
||||
171 | } |
||||
172 | |||||
173 | if (is_string($response)) { |
||||
174 | $response = [ |
||||
175 | 'status' => $response, |
||||
176 | ]; |
||||
177 | } |
||||
178 | |||||
179 | $response['op'] = $op; |
||||
180 | if (isset($request['id'])) { |
||||
181 | $response['id'] = (string) $request['id']; |
||||
182 | } |
||||
183 | $response['domain'] = $this->domain; |
||||
184 | |||||
185 | $this->send($return, 'CDCResponse', $response); |
||||
186 | } |
||||
187 | |||||
188 | |||||
189 | /** |
||||
190 | * Handle an append request. |
||||
191 | * |
||||
192 | * @param array $request The request. |
||||
193 | * @throws \SimpleSAML\Error\BadRequest |
||||
194 | * @return string The response. |
||||
195 | */ |
||||
196 | private function handleAppend(array $request): string |
||||
197 | { |
||||
198 | if (!isset($request['entityID'])) { |
||||
199 | throw new Error\BadRequest('Missing entityID in append request.'); |
||||
200 | } |
||||
201 | $entityID = (string) $request['entityID']; |
||||
202 | |||||
203 | $list = $this->getCDC(); |
||||
204 | |||||
205 | $prevIndex = array_search($entityID, $list, true); |
||||
206 | if ($prevIndex !== false) { |
||||
207 | unset($list[$prevIndex]); |
||||
208 | } |
||||
209 | $list[] = $entityID; |
||||
210 | |||||
211 | $this->setCDC($list); |
||||
212 | |||||
213 | return 'ok'; |
||||
214 | } |
||||
215 | |||||
216 | |||||
217 | /** |
||||
218 | * Handle a delete request. |
||||
219 | * |
||||
220 | * @param array $request The request. |
||||
221 | * @return string The response. |
||||
222 | */ |
||||
223 | private function handleDelete(array $request): string |
||||
0 ignored issues
–
show
|
|||||
224 | { |
||||
225 | $params = [ |
||||
226 | 'path' => '/', |
||||
227 | 'domain' => '.' . $this->domain, |
||||
228 | 'secure' => true, |
||||
229 | 'httponly' => false, |
||||
230 | ]; |
||||
231 | |||||
232 | $httpUtils = new Utils\HTTP(); |
||||
233 | $httpUtils->setCookie('_saml_idp', null, $params, false); |
||||
234 | return 'ok'; |
||||
235 | } |
||||
236 | |||||
237 | |||||
238 | /** |
||||
239 | * Handle a read request. |
||||
240 | * |
||||
241 | * @param array $request The request. |
||||
242 | * @return array The response. |
||||
243 | */ |
||||
244 | private function handleRead(array $request): array |
||||
0 ignored issues
–
show
The parameter
$request is not used and could be removed.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for parameters that have been defined for a function or method, but which are not used in the method body. ![]() |
|||||
245 | { |
||||
246 | $list = $this->getCDC(); |
||||
247 | |||||
248 | return [ |
||||
249 | 'status' => 'ok', |
||||
250 | 'cdc' => $list, |
||||
251 | ]; |
||||
252 | } |
||||
253 | |||||
254 | |||||
255 | /** |
||||
256 | * Helper function for parsing and validating a CDC message. |
||||
257 | * |
||||
258 | * @param string $parameter The name of the query parameter. |
||||
259 | * @throws \SimpleSAML\Error\BadRequest |
||||
260 | * @return array|null The response, or NULL if no response is received. |
||||
261 | */ |
||||
262 | private static function get(string $parameter): ?array |
||||
263 | { |
||||
264 | if (!isset($_REQUEST[$parameter])) { |
||||
265 | return null; |
||||
266 | } |
||||
267 | $message = (string) $_REQUEST[$parameter]; |
||||
268 | Assert::validBase64($message, ProtocolViolationException::class); |
||||
269 | |||||
270 | $message = @base64_decode($message); |
||||
271 | if ($message === false) { |
||||
272 | throw new Error\BadRequest('Error base64-decoding CDC message.'); |
||||
273 | } |
||||
274 | |||||
275 | $message = @json_decode($message, true); |
||||
276 | if ($message === false) { |
||||
277 | throw new Error\BadRequest('Error json-decoding CDC message.'); |
||||
278 | } |
||||
279 | |||||
280 | if (!isset($message['timestamp'])) { |
||||
281 | throw new Error\BadRequest('Missing timestamp in CDC message.'); |
||||
282 | } |
||||
283 | $timestamp = (int) $message['timestamp']; |
||||
284 | |||||
285 | if ($timestamp + 60 < time()) { |
||||
286 | throw new Error\BadRequest('CDC signature has expired.'); |
||||
287 | } |
||||
288 | if ($timestamp - 60 > time()) { |
||||
289 | throw new Error\BadRequest('CDC signature from the future.'); |
||||
290 | } |
||||
291 | |||||
292 | if (!isset($message['domain'])) { |
||||
293 | throw new Error\BadRequest('Missing domain in CDC message.'); |
||||
294 | } |
||||
295 | |||||
296 | return $message; |
||||
297 | } |
||||
298 | |||||
299 | |||||
300 | /** |
||||
301 | * Helper function for validating the signature on a CDC message. |
||||
302 | * |
||||
303 | * Will throw an exception if the message is invalid. |
||||
304 | * |
||||
305 | * @param string $parameter The name of the query parameter. |
||||
306 | * @throws \SimpleSAML\Error\BadRequest |
||||
307 | */ |
||||
308 | private function validate(string $parameter): void |
||||
309 | { |
||||
310 | Assert::keyExists($_REQUEST, $parameter); |
||||
311 | |||||
312 | $message = (string) $_REQUEST[$parameter]; |
||||
313 | |||||
314 | if (!isset($_REQUEST['Signature'])) { |
||||
315 | throw new Error\BadRequest('Missing Signature on CDC message.'); |
||||
316 | } |
||||
317 | $signature = (string) $_REQUEST['Signature']; |
||||
318 | |||||
319 | $cSignature = $this->calcSignature($message); |
||||
320 | if ($signature !== $cSignature) { |
||||
321 | throw new Error\BadRequest('Invalid signature on CDC message.'); |
||||
322 | } |
||||
323 | } |
||||
324 | |||||
325 | |||||
326 | /** |
||||
327 | * Helper function for sending CDC messages. |
||||
328 | * |
||||
329 | * @param string $to The URL the message should be delivered to. |
||||
330 | * @param string $parameter The query parameter the message should be sent in. |
||||
331 | * @param array $message The CDC message. |
||||
332 | */ |
||||
333 | private function send(string $to, string $parameter, array $message): void |
||||
334 | { |
||||
335 | $message['timestamp'] = time(); |
||||
336 | $message = json_encode($message); |
||||
337 | $message = base64_encode($message); |
||||
338 | |||||
339 | $signature = $this->calcSignature($message); |
||||
340 | |||||
341 | $params = [ |
||||
342 | $parameter => $message, |
||||
343 | 'Signature' => $signature, |
||||
344 | ]; |
||||
345 | |||||
346 | $httpUtils = new Utils\HTTP(); |
||||
347 | $url = $httpUtils->addURLParameters($to, $params); |
||||
348 | if (strlen($url) < 2048) { |
||||
349 | $httpUtils->redirectTrustedURL($url); |
||||
350 | } else { |
||||
351 | $httpUtils->submitPOSTData($to, $params); |
||||
352 | } |
||||
353 | } |
||||
354 | |||||
355 | |||||
356 | /** |
||||
357 | * Calculate the signature on the given message. |
||||
358 | * |
||||
359 | * @param string $rawMessage The base64-encoded message. |
||||
360 | * @return string The signature. |
||||
361 | */ |
||||
362 | private function calcSignature(string $rawMessage): string |
||||
363 | { |
||||
364 | return sha1($this->key . $rawMessage . $this->key); |
||||
365 | } |
||||
366 | |||||
367 | |||||
368 | /** |
||||
369 | * Get the IdP entities saved in the common domain cookie. |
||||
370 | * |
||||
371 | * @return array List of IdP entities. |
||||
372 | */ |
||||
373 | private function getCDC(): array |
||||
374 | { |
||||
375 | if (!isset($_COOKIE['_saml_idp'])) { |
||||
376 | return []; |
||||
377 | } |
||||
378 | |||||
379 | $ret = (string) $_COOKIE['_saml_idp']; |
||||
380 | |||||
381 | $ret = explode(' ', $ret); |
||||
382 | foreach ($ret as &$idp) { |
||||
383 | Assert::validBase64($idp, ProtocolViolationException::class); |
||||
384 | $idp = base64_decode($idp); |
||||
385 | if ($idp === false) { |
||||
386 | // Not properly base64 encoded |
||||
387 | Logger::warning('CDC - Invalid base64-encoding of CDC entry.'); |
||||
388 | return []; |
||||
389 | } |
||||
390 | Assert::validURI($idp, ProtocolViolationException::class); |
||||
391 | } |
||||
392 | |||||
393 | return $ret; |
||||
394 | } |
||||
395 | |||||
396 | |||||
397 | /** |
||||
398 | * Build a CDC cookie string. |
||||
399 | * |
||||
400 | * @param array $list The list of IdPs. |
||||
401 | * @return string The CDC cookie value. |
||||
402 | */ |
||||
403 | private function setCDC(array $list): string |
||||
404 | { |
||||
405 | foreach ($list as &$value) { |
||||
406 | $value = base64_encode($value); |
||||
407 | } |
||||
408 | |||||
409 | $cookie = implode(' ', $list); |
||||
410 | |||||
411 | while (strlen($cookie) > 4000) { |
||||
412 | // The cookie is too long. Remove the oldest elements until it is short enough |
||||
413 | $tmp = explode(' ', $cookie, 2); |
||||
414 | if (count($tmp) === 1) { |
||||
415 | /* |
||||
416 | * We are left with a single entityID whose base64 |
||||
417 | * representation is too long to fit in a cookie. |
||||
418 | */ |
||||
419 | break; |
||||
420 | } |
||||
421 | $cookie = $tmp[1]; |
||||
422 | } |
||||
423 | |||||
424 | $params = [ |
||||
425 | 'lifetime' => $this->cookieLifetime, |
||||
426 | 'path' => '/', |
||||
427 | 'domain' => '.' . $this->domain, |
||||
428 | 'secure' => true, |
||||
429 | 'httponly' => false, |
||||
430 | ]; |
||||
431 | |||||
432 | $httpUtils = new Utils\HTTP(); |
||||
433 | $httpUtils->setCookie('_saml_idp', $cookie, $params, false); |
||||
434 | |||||
435 | return '_saml_idp'; |
||||
436 | } |
||||
437 | } |
||||
438 |
This check looks for parameters that have been defined for a function or method, but which are not used in the method body.