Issues (26)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  Header Injection
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Shib13/AuthnResponse.php (10 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\casserver\Shib13;
6
7
use DOMDocument;
8
use DOMNode;
9
use DOMNodeList;
10
use DOMXPath;
11
use Exception;
12
use SimpleSAML\Assert\Assert;
13
use SimpleSAML\Configuration;
14
use SimpleSAML\Error;
15
use SimpleSAML\Metadata\MetaDataStorageHandler;
16
use SimpleSAML\Utils;
17
use SimpleSAML\XML\DOMDocumentFactory;
18
use SimpleSAML\XML\Utils as XMLUtils;
19
use SimpleSAML\XML\Utils\Random;
20
use SimpleSAML\XML\Validator;
21
use SimpleXMLElement;
22
23
/**
24
 * A Shibboleth 1.3 authentication response.
25
 *
26
 * @package SimpleSAMLphp
27
 */
28
class AuthnResponse
29
{
30
    /**
31
     * @var \SimpleSAML\XML\Validator|null This variable contains an XML validator for this message.
32
     */
33
    private ?Validator $validator = null;
34
35
    /**
36
     * @var bool Whether this response was validated by some external means (e.g. SSL).
37
     */
38
    private bool $messageValidated = false;
39
40
    /** @var string */
41
    public const SHIB_PROTOCOL_NS = 'urn:oasis:names:tc:SAML:1.0:protocol';
42
43
    /** @var string */
44
    public const SHIB_ASSERT_NS = 'urn:oasis:names:tc:SAML:1.0:assertion';
45
46
    /**
47
     * @var \DOMDocument|null The DOMDocument which represents this message.
48
     */
49
    private ?DOMDocument $dom = null;
50
51
    /**
52
     * @var string|null The relaystate which is associated with this response.
53
     */
54
    private ?string $relayState = null;
55
56
57
    /**
58
     * Set whether this message was validated externally.
59
     *
60
     * @param bool $messageValidated  TRUE if the message is already validated, FALSE if not.
61
     */
62
    public function setMessageValidated(bool $messageValidated): void
63
    {
64
        $this->messageValidated = $messageValidated;
65
    }
66
67
68
    /**
69
     * @param string $xml
70
     * @throws \Exception
71
     */
72
    public function setXML(string $xml): void
73
    {
74
        try {
75
            $this->dom = DOMDocumentFactory::fromString(str_replace("\r", "", $xml));
76
        } catch (Exception $e) {
77
            throw new Exception('Unable to parse AuthnResponse XML.');
78
        }
79
    }
80
81
82
    /**
83
     * @param string|null $relayState
84
     */
85
    public function setRelayState(?string $relayState): void
86
    {
87
        $this->relayState = $relayState;
88
    }
89
90
91
    /**
92
     * @return string|null
93
     */
94
    public function getRelayState(): ?string
95
    {
96
        return $this->relayState;
97
    }
98
99
100
    /**
101
     * @throws \SimpleSAML\Error\Exception
102
     * @return bool
103
     */
104
    public function validate(): bool
105
    {
106
        Assert::isInstanceOf($this->dom, DOMDocument::class);
107
108
        if ($this->messageValidated) {
109
            // This message was validated externally
110
            return true;
111
        }
112
113
        // Validate the signature
114
        $this->validator = new Validator($this->dom, ['ResponseID', 'AssertionID']);
0 ignored issues
show
It seems like $this->dom can also be of type null; however, parameter $xmlNode of SimpleSAML\XML\Validator::__construct() does only seem to accept DOMDocument, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

114
        $this->validator = new Validator(/** @scrutinizer ignore-type */ $this->dom, ['ResponseID', 'AssertionID']);
Loading history...
115
116
        // Get the issuer of the response
117
        $issuer = $this->getIssuer();
118
119
        // Get the metadata of the issuer
120
        $metadata = MetaDataStorageHandler::getMetadataHandler();
121
        $md = $metadata->getMetaDataConfig($issuer, 'shib13-idp-remote');
122
123
        $publicKeys = $md->getPublicKeys('signing');
124
        if (!empty($publicKeys)) {
125
            $certFingerprints = [];
126
            foreach ($publicKeys as $key) {
127
                if ($key['type'] !== 'X509Certificate') {
128
                    continue;
129
                }
130
                $certFingerprints[] = sha1(base64_decode($key['X509Certificate']));
131
            }
132
            $this->validator->validateFingerprint($certFingerprints);
0 ignored issues
show
The method validateFingerprint() does not exist on SimpleSAML\XML\Validator. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

132
            $this->validator->/** @scrutinizer ignore-call */ 
133
                              validateFingerprint($certFingerprints);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
133
        } elseif ($md->hasValue('certFingerprint')) {
134
            $certFingerprints = $md->getArrayizeString('certFingerprint');
135
136
            // Validate the fingerprint
137
            $this->validator->validateFingerprint($certFingerprints);
138
        } elseif ($md->hasValue('caFile')) {
139
            // Validate against CA
140
            $configUtils = new Utils\Config();
141
            $this->validator->validateCA($configUtils->getCertPath($md->getString('caFile')));
0 ignored issues
show
The method validateCA() does not exist on SimpleSAML\XML\Validator. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

141
            $this->validator->/** @scrutinizer ignore-call */ 
142
                              validateCA($configUtils->getCertPath($md->getString('caFile')));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
142
        } else {
143
            throw new Error\Exception(
144
                'Missing certificate in Shibboleth 1.3 IdP Remote metadata for identity provider [' . $issuer . '].',
145
            );
146
        }
147
148
        return true;
149
    }
150
151
152
    /**
153
     * Checks if the given node is validated by the signature on this response.
154
     *
155
     * @param \DOMElement|\SimpleXMLElement $node Node to be validated.
156
     * @return bool TRUE if the node is validated or FALSE if not.
157
     */
158
    private function isNodeValidated($node): bool
159
    {
160
        if ($this->messageValidated) {
161
            // This message was validated externally
162
            return true;
163
        }
164
165
        if ($this->validator === null) {
166
            return false;
167
        }
168
169
        // Convert the node to a DOM node if it is an element from SimpleXML
170
        if ($node instanceof SimpleXMLElement) {
171
            $node = dom_import_simplexml($node);
172
        }
173
174
        Assert::isInstanceOf($node, DOMNode::class);
175
176
        return $this->validator->isNodeValidated($node);
0 ignored issues
show
It seems like $node can also be of type null; however, parameter $node of SimpleSAML\XML\Validator::isNodeValidated() does only seem to accept DOMNode, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

176
        return $this->validator->isNodeValidated(/** @scrutinizer ignore-type */ $node);
Loading history...
177
    }
178
179
180
    /**
181
     * This function runs an xPath query on this authentication response.
182
     *
183
     * @param string $query   The query which should be run.
184
     * @param \DOMNode $node  The node which this query is relative to. If this node is NULL (the default)
185
     *                        then the query will be relative to the root of the response.
186
     * @return \DOMNodeList
187
     */
188
    private function doXPathQuery(string $query, DOMNode $node = null): DOMNodeList
189
    {
190
        Assert::isInstanceOf($node, DOMNode::class);
191
192
        if ($node === null) {
193
            $node = $this->dom->documentElement;
194
        }
195
196
        Assert::isInstanceOf($node, DOMNode::class);
197
198
        $xPath = new DOMXpath($this->dom);
0 ignored issues
show
It seems like $this->dom can also be of type null; however, parameter $document of DOMXPath::__construct() does only seem to accept DOMDocument, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

198
        $xPath = new DOMXpath(/** @scrutinizer ignore-type */ $this->dom);
Loading history...
199
        $xPath->registerNamespace('shibp', self::SHIB_PROTOCOL_NS);
200
        $xPath->registerNamespace('shib', self::SHIB_ASSERT_NS);
201
202
        return $xPath->query($query, $node);
203
    }
204
205
206
    /**
207
     * Retrieve the session index of this response.
208
     *
209
     * @return string|null  The session index of this response.
210
     */
211
    public function getSessionIndex(): ?string
212
    {
213
        $query = '/shibp:Response/shib:Assertion/shib:AuthnStatement';
214
        $nodelist = $this->doXPathQuery($query);
215
216
        if ($node = $nodelist->item(0)) {
217
            return $node->getAttribute('SessionIndex');
218
        }
219
220
        return null;
221
    }
222
223
224
    /**
225
     * @throws \Exception
226
     * @return array
227
     */
228
    public function getAttributes(): array
229
    {
230
        $metadata = MetaDataStorageHandler::getMetadataHandler();
231
        $md = $metadata->getMetaData($this->getIssuer(), 'shib13-idp-remote');
232
        $base64 = isset($md['base64attributes']) ? $md['base64attributes'] : false;
233
234
        if (!($this->dom instanceof DOMDocument)) {
235
            return [];
236
        }
237
238
        $attributes = [];
239
240
        $assertions = $this->doXPathQuery('/shibp:Response/shib:Assertion');
241
242
        foreach ($assertions as $assertion) {
243
            if (!$this->isNodeValidated($assertion)) {
244
                throw new Exception('Shib13 AuthnResponse contained an unsigned assertion.');
245
            }
246
247
            $conditions = $this->doXPathQuery('shib:Conditions', $assertion);
248
            if ($conditions->length > 0) {
249
                $condition = $conditions->item(0);
250
251
                $start = $condition->getAttribute('NotBefore');
252
                $end = $condition->getAttribute('NotOnOrAfter');
253
254
                if ($start && $end) {
255
                    if (!self::checkDateConditions($start, $end)) {
256
                        error_log('Date check failed ... (from ' . $start . ' to ' . $end . ')');
257
                        continue;
258
                    }
259
                }
260
            }
261
262
            $attribute_nodes = $this->doXPathQuery(
263
                'shib:AttributeStatement/shib:Attribute/shib:AttributeValue',
264
                $assertion,
265
            );
266
267
            foreach ($attribute_nodes as $attribute) {
268
                /** @var \DOMElement $attribute */
269
270
                $value = $attribute->textContent;
271
                /** @var \DOMElement $parentNode */
272
                $parentNode = $attribute->parentNode;
273
                $name = $parentNode->getAttribute('AttributeName');
274
275
                if ($attribute->hasAttribute('Scope')) {
276
                    $scopePart = '@' . $attribute->getAttribute('Scope');
277
                } else {
278
                    $scopePart = '';
279
                }
280
281
                if (empty($name)) {
282
                    throw new Exception('Shib13 Attribute node without an AttributeName.');
283
                }
284
285
                if (!array_key_exists($name, $attributes)) {
286
                    $attributes[$name] = [];
287
                }
288
289
                if ($base64) {
290
                    $encodedvalues = explode('_', $value);
291
                    foreach ($encodedvalues as $v) {
292
                        $attributes[$name][] = base64_decode($v) . $scopePart;
293
                    }
294
                } else {
295
                    $attributes[$name][] = $value . $scopePart;
296
                }
297
            }
298
        }
299
300
        return $attributes;
301
    }
302
303
304
    /**
305
     * @throws \Exception
306
     * @return string
307
     */
308
    public function getIssuer(): string
309
    {
310
        $query = '/shibp:Response/shib:Assertion/@Issuer';
311
        $nodelist = $this->doXPathQuery($query);
312
313
        if ($attr = $nodelist->item(0)) {
314
            return $attr->value;
315
        } else {
316
            throw new Exception('Could not find Issuer field in Authentication response');
317
        }
318
    }
319
320
321
    /**
322
     * @return array
323
     */
324
    public function getNameID(): array
325
    {
326
        $nameID = [];
327
328
        $query = '/shibp:Response/shib:Assertion/shib:AuthenticationStatement/shib:Subject/shib:NameIdentifier';
329
        $nodelist = $this->doXPathQuery($query);
330
331
        if ($node = $nodelist->item(0)) {
332
            $nameID["Value"] = $node->nodeValue;
333
            $nameID["Format"] = $node->getAttribute('Format');
334
        }
335
336
        return $nameID;
337
    }
338
339
340
    /**
341
     * Build a authentication response.
342
     *
343
     * @param \SimpleSAML\Configuration $idp Metadata for the IdP the response is sent from.
344
     * @param \SimpleSAML\Configuration $sp Metadata for the SP the response is sent to.
345
     * @param string $shire The endpoint on the SP the response is sent to.
346
     * @param array|null $attributes The attributes which should be included in the response.
347
     * @return string The response.
348
     */
349
    public function generate(Configuration $idp, Configuration $sp, string $shire, ?array $attributes): string
350
    {
351
        if ($sp->hasValue('scopedattributes')) {
352
            $scopedAttributes = $sp->getArray('scopedattributes');
353
        } elseif ($idp->hasValue('scopedattributes')) {
354
            $scopedAttributes = $idp->getArray('scopedattributes');
355
        } else {
356
            $scopedAttributes = [];
357
        }
358
359
        $randomUtils = new Random();
360
        $timeUtils = new Utils\Time();
361
362
        $id = $randomUtils->generateID();
363
        $issueInstant = $timeUtils->generateTimestamp();
364
365
        // 30 seconds timeskew back in time to allow differing clocks
366
        $notBefore = $timeUtils->generateTimestamp(time() - 30);
367
368
        $assertionExpire = $timeUtils->generateTimestamp(time() + 300); // 5 minutes
369
        $assertionid = $randomUtils->generateID();
370
371
        $spEntityId = $sp->getString('entityid');
372
373
        $audience = $sp->getOptionalString('audience', $spEntityId);
374
        $base64 = $sp->getOptionalBoolean('base64attributes', false);
375
376
        $namequalifier = $sp->getOptionalString('NameQualifier', $spEntityId);
377
        $nameid = $randomUtils->generateID();
378
        $subjectNode =
379
            '<Subject>' .
380
            '<NameIdentifier' .
381
            ' Format="urn:mace:shibboleth:1.0:nameIdentifier"' .
382
            ' NameQualifier="' . htmlspecialchars($namequalifier) . '"' .
0 ignored issues
show
It seems like $namequalifier can also be of type null; however, parameter $string of htmlspecialchars() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

382
            ' NameQualifier="' . htmlspecialchars(/** @scrutinizer ignore-type */ $namequalifier) . '"' .
Loading history...
383
            '>' .
384
            htmlspecialchars($nameid) .
385
            '</NameIdentifier>' .
386
            '<SubjectConfirmation>' .
387
            '<ConfirmationMethod>' .
388
            'urn:oasis:names:tc:SAML:1.0:cm:bearer' .
389
            '</ConfirmationMethod>' .
390
            '</SubjectConfirmation>' .
391
            '</Subject>';
392
393
        $encodedattributes = '';
394
395
        if (is_array($attributes)) {
0 ignored issues
show
The condition is_array($attributes) is always true.
Loading history...
396
            $encodedattributes .= '<AttributeStatement>';
397
            $encodedattributes .= $subjectNode;
398
399
            foreach ($attributes as $name => $value) {
400
                $encodedattributes .= $this->encAttribute($name, $value, $base64, $scopedAttributes);
0 ignored issues
show
It seems like $base64 can also be of type null; however, parameter $base64 of SimpleSAML\Module\casser...esponse::encAttribute() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

400
                $encodedattributes .= $this->encAttribute($name, $value, /** @scrutinizer ignore-type */ $base64, $scopedAttributes);
Loading history...
401
            }
402
403
            $encodedattributes .= '</AttributeStatement>';
404
        }
405
406
        /*
407
         * The SAML 1.1 response message
408
         */
409
        $response = '<Response xmlns="urn:oasis:names:tc:SAML:1.0:protocol"
410
    xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion"
411
    xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
412
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" IssueInstant="' . $issueInstant . '"
413
    MajorVersion="1" MinorVersion="1"
414
    Recipient="' . htmlspecialchars($shire) . '" ResponseID="' . $id . '">
415
    <Status>
416
        <StatusCode Value="samlp:Success" />
417
    </Status>
418
    <Assertion xmlns="urn:oasis:names:tc:SAML:1.0:assertion"
419
        AssertionID="' . $assertionid . '" IssueInstant="' . $issueInstant . '"
420
        Issuer="' . htmlspecialchars($idp->getString('entityid')) . '" MajorVersion="1" MinorVersion="1">
421
        <Conditions NotBefore="' . $notBefore . '" NotOnOrAfter="' . $assertionExpire . '">
422
            <AudienceRestrictionCondition>
423
                <Audience>' . htmlspecialchars($audience) . '</Audience>
424
            </AudienceRestrictionCondition>
425
        </Conditions>
426
        <AuthenticationStatement AuthenticationInstant="' . $issueInstant . '"
427
            AuthenticationMethod="urn:oasis:names:tc:SAML:1.0:am:unspecified">' .
428
            $subjectNode . '
429
        </AuthenticationStatement>
430
        ' . $encodedattributes . '
431
    </Assertion>
432
</Response>';
433
434
        return $response;
435
    }
436
437
438
    /**
439
     * Format a shib13 attribute.
440
     *
441
     * @param string $name  Name of the attribute.
442
     * @param array $values  Values of the attribute (as an array of strings).
443
     * @param bool $base64  Whether the attriubte values should be base64-encoded.
444
     * @param array $scopedAttributes  Array of attributes names which are scoped.
445
     * @return string  The attribute encoded as an XML-string.
446
     */
447
    private function encAttribute(string $name, array $values, bool $base64, array $scopedAttributes): string
448
    {
449
        if (in_array($name, $scopedAttributes, true)) {
450
            $scoped = true;
451
        } else {
452
            $scoped = false;
453
        }
454
455
        $attr = '<Attribute AttributeName="' . htmlspecialchars($name) .
456
            '" AttributeNamespace="urn:mace:shibboleth:1.0:attributeNamespace:uri">';
457
        foreach ($values as $value) {
458
            $scopePart = '';
459
            if ($scoped) {
460
                $tmp = explode('@', $value, 2);
461
                if (count($tmp) === 2) {
462
                    $value = $tmp[0];
463
                    $scopePart = ' Scope="' . htmlspecialchars($tmp[1]) . '"';
464
                }
465
            }
466
467
            if ($base64) {
468
                $value = base64_encode($value);
469
            }
470
471
            $attr .= '<AttributeValue' . $scopePart . '>' . htmlspecialchars($value) . '</AttributeValue>';
472
        }
473
        $attr .= '</Attribute>';
474
475
        return $attr;
476
    }
477
478
    /**
479
     * Check if we are currently between the given date & time conditions.
480
     *
481
     * Note that this function allows a 10-minute leap from the initial time as marked by $start.
482
     *
483
     * @param string|null $start A SAML2 timestamp marking the start of the period to check. Defaults to null, in which
484
     *     case there's no limitations in the past.
485
     * @param string|null $end A SAML2 timestamp marking the end of the period to check. Defaults to null, in which
486
     *     case there's no limitations in the future.
487
     *
488
     * @return bool True if the current time belongs to the period specified by $start and $end. False otherwise.
489
     *
490
     * @see \SAML2\Utils::xsDateTimeToTimestamp.
491
     *
492
     */
493
    protected static function checkDateConditions(string $start = null, string $end = null): bool
494
    {
495
        $currentTime = time();
496
497
        if (!empty($start)) {
498
            $startTime = XMLUtils::xsDateTimeToTimestamp($start);
0 ignored issues
show
Deprecated Code introduced by
The function SimpleSAML\XML\Utils::xsDateTimeToTimestamp() has been deprecated: Use DateTime objects instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

498
            $startTime = /** @scrutinizer ignore-deprecated */ XMLUtils::xsDateTimeToTimestamp($start);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
499
            // allow for a 10 minute difference in time
500
            if (($startTime < 0) || (($startTime - 600) > $currentTime)) {
501
                return false;
502
            }
503
        }
504
        if (!empty($end)) {
505
            $endTime = XMLUtils::xsDateTimeToTimestamp($end);
0 ignored issues
show
Deprecated Code introduced by
The function SimpleSAML\XML\Utils::xsDateTimeToTimestamp() has been deprecated: Use DateTime objects instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

505
            $endTime = /** @scrutinizer ignore-deprecated */ XMLUtils::xsDateTimeToTimestamp($end);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
506
            if (($endTime < 0) || ($endTime <= $currentTime)) {
507
                return false;
508
            }
509
        }
510
        return true;
511
    }
512
}
513