SecurityStatus.php 13.2 KB
Newer Older
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
9
10
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
11
 *
12
13
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
16
 * The TYPO3 project - inspiring people to share!
 */
17

18
19
namespace TYPO3\CMS\Reports\Report\Status;

20
use Psr\Http\Message\ServerRequestInterface;
Christian Kuhn's avatar
Christian Kuhn committed
21
use TYPO3\CMS\Backend\Routing\UriBuilder;
22
use TYPO3\CMS\Core\Core\Environment;
23
24
use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException;
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
25
26
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
27
use TYPO3\CMS\Core\Localization\LanguageService;
28
use TYPO3\CMS\Core\Middleware\VerifyHostHeader;
29
use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
30
use TYPO3\CMS\Core\Utility\GeneralUtility;
31
use TYPO3\CMS\Reports\RequestAwareStatusProviderInterface;
32
use TYPO3\CMS\Reports\Status as ReportStatus;
33

34
35
36
/**
 * Performs several checks about the system's health
 */
37
class SecurityStatus implements RequestAwareStatusProviderInterface
38
{
39
40
41
42
43
    /**
     * @var ServerRequestInterface
     */
    protected $request;

44
45
46
    /**
     * Determines the security of this TYPO3 installation
     *
47
48
     * @param ServerRequestInterface|null $request
     * @return ReportStatus[] List of statuses
49
     */
50
    public function getStatus(ServerRequestInterface $request = null): array
51
    {
52
        $statuses = [
53
54
55
56
            'trustedHostsPattern' => $this->getTrustedHostsPatternStatus(),
            'adminUserAccount' => $this->getAdminAccountStatus(),
            'fileDenyPattern' => $this->getFileDenyPatternStatus(),
            'htaccessUpload' => $this->getHtaccessUploadStatus(),
57
            'exceptionHandler' => $this->getExceptionHandlerStatus(),
58
            'exportedFiles' => $this->getExportedFilesStatus(),
59
        ];
60
61
62

        if ($request !== null) {
            $statuses['encryptedConnectionStatus'] = $this->getEncryptedConnectionStatus($request);
63
64
65
66
            $lockSslStatus = $this->getLockSslStatus($request);
            if ($lockSslStatus) {
                $statuses['getLockSslStatus'] = $lockSslStatus;
            }
67
68
        }

69
70
        return $statuses;
    }
71

72
73
74
75
76
    public function getLabel(): string
    {
        return 'security';
    }

77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
    /**
     * Checks if the current connection is encrypted (HTTPS)
     *
     * @param ServerRequestInterface $request
     * @return ReportStatus
     */
    protected function getEncryptedConnectionStatus(ServerRequestInterface $request): ReportStatus
    {
        $value = $this->getLanguageService()->getLL('status_ok');
        $message = '';
        $severity = ReportStatus::OK;

        $normalizedParams = $request->getAttribute('normalizedParams');

        if (!$normalizedParams->isHttps()) {
            $value = $this->getLanguageService()->getLL('status_insecure');
            $severity = ReportStatus::WARNING;
            $message = $this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_encryptedConnectionStatus_insecure');
        }

        return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_encryptedConnectionStatus'), $value, $message, $severity);
    }

100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
    /**
     * @param ServerRequestInterface $request
     * @return ReportStatus
     */
    protected function getLockSslStatus(ServerRequestInterface $request): ?ReportStatus
    {
        $normalizedParams = $request->getAttribute('normalizedParams');

        if ($normalizedParams->isHttps()) {
            $value = $this->getLanguageService()->getLL('status_ok');
            $message = '';
            $severity = ReportStatus::OK;

            if (!$GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSL']) {
                $value = $this->getLanguageService()->getLL('status_insecure');
                $message = $this->getLanguageService()->getLL('status_lockSslStatus_insecure');
                $severity = ReportStatus::WARNING;
            }

            return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_lockSslStatus'), $value, $message, $severity);
        }

        return null;
    }

125
126
127
    /**
     * Checks if the trusted hosts pattern check is disabled.
     *
128
     * @return ReportStatus An object representing whether the check is disabled
129
     */
130
    protected function getTrustedHostsPatternStatus(): ReportStatus
131
    {
132
        $value = $this->getLanguageService()->getLL('status_ok');
133
        $message = '';
134
        $severity = ReportStatus::OK;
135

136
        if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] === VerifyHostHeader::ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL) {
137
            $value = $this->getLanguageService()->getLL('status_insecure');
138
            $severity = ReportStatus::ERROR;
139
            $message = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.install_trustedhosts');
140
        }
141

142
        return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_trustedHostsPattern'), $value, $message, $severity);
143
    }
144

145
146
147
    /**
     * Checks whether a BE user account named admin with default password exists.
     *
148
     * @return ReportStatus An object representing whether a default admin account exists
149
     */
150
    protected function getAdminAccountStatus(): ReportStatus
151
    {
152
        $value = $this->getLanguageService()->getLL('status_ok');
153
        $message = '';
154
        $severity = ReportStatus::OK;
155
156
157
158
159
160
161
162
163
164
165
166

        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
        $queryBuilder->getRestrictions()
            ->removeAll()
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));

        $row = $queryBuilder
            ->select('uid', 'username', 'password')
            ->from('be_users')
            ->where(
                $queryBuilder->expr()->eq(
                    'username',
167
                    $queryBuilder->createNamedParameter('admin', \PDO::PARAM_STR)
168
169
                )
            )
170
            ->executeQuery()
171
            ->fetchAssociative();
172

173
        if (!empty($row)) {
Christian Kuhn's avatar
Christian Kuhn committed
174
            try {
175
                $hashInstance = GeneralUtility::makeInstance(PasswordHashFactory::class)->get($row['password'], 'BE');
Christian Kuhn's avatar
Christian Kuhn committed
176
177
178
179
180
181
182
183
184
185
                if ($hashInstance->checkPassword('password', $row['password'])) {
                    // If the password for 'admin' user is 'password': bad idea!
                    // We're checking since the (very) old installer created instances like this in dark old times.
                    $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
                    $value = $this->getLanguageService()->getLL('status_insecure');
                    $severity = ReportStatus::ERROR;
                    $editUserAccountUrl = (string)$uriBuilder->buildUriFromRoute(
                        'record_edit',
                        [
                            'edit[be_users][' . $row['uid'] . ']' => 'edit',
186
                            'returnUrl' => (string)$uriBuilder->buildUriFromRoute('system_reports'),
Christian Kuhn's avatar
Christian Kuhn committed
187
188
189
190
191
192
193
                        ]
                    );
                    $message = sprintf(
                        $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.backend_admin'),
                        '<a href="' . htmlspecialchars($editUserAccountUrl) . '">',
                        '</a>'
                    );
194
                }
195
            } catch (InvalidPasswordHashException $e) {
Christian Kuhn's avatar
Christian Kuhn committed
196
                // No hash class handling for current hash could be found. Not good, but ok in this case.
197
198
            }
        }
199

200
        return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_adminUserAccount'), $value, $message, $severity);
201
    }
202

203
204
205
    /**
     * Checks if fileDenyPattern was changed which is dangerous on Apache
     *
206
     * @return ReportStatus An object representing whether the file deny pattern has changed
207
     */
208
    protected function getFileDenyPatternStatus(): ReportStatus
209
    {
210
        $value = $this->getLanguageService()->getLL('status_ok');
211
        $message = '';
212
        $severity = ReportStatus::OK;
213

214
215
        $fileAccessCheck = GeneralUtility::makeInstance(FileNameValidator::class);
        if ($fileAccessCheck->missingImportantPatterns()) {
216
            $value = $this->getLanguageService()->getLL('status_insecure');
217
            $severity = ReportStatus::ERROR;
218
            $message = sprintf(
219
                $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_deny_pattern_partsNotPresent'),
220
                '<br /><pre>' . htmlspecialchars($fileAccessCheck::DEFAULT_FILE_DENY_PATTERN) . '</pre><br />'
221
            );
222
        }
223

224
        return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_fileDenyPattern'), $value, $message, $severity);
225
    }
226

227
228
229
230
    /**
     * Checks if fileDenyPattern allows to upload .htaccess files which is
     * dangerous on Apache.
     *
231
     * @return ReportStatus An object representing whether it's possible to upload .htaccess files
232
     */
233
    protected function getHtaccessUploadStatus(): ReportStatus
234
    {
235
        $value = $this->getLanguageService()->getLL('status_ok');
236
        $message = '';
237
        $severity = ReportStatus::OK;
238

239
240
241
        $fileNameAccess = GeneralUtility::makeInstance(FileNameValidator::class);
        if ($fileNameAccess->customFileDenyPatternConfigured()
            && $fileNameAccess->isValid('.htaccess')) {
242
            $value = $this->getLanguageService()->getLL('status_insecure');
243
            $severity = ReportStatus::ERROR;
244
            $message = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_deny_htaccess');
245
        }
246

247
        return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_htaccessUploadProtection'), $value, $message, $severity);
248
    }
249

250
251
252
253
254
255
    protected function getExceptionHandlerStatus(): ReportStatus
    {
        $value = $this->getLanguageService()->getLL('status_ok');
        $message = '';
        $severity = ReportStatus::OK;
        if (
256
            str_contains($GLOBALS['TYPO3_CONF_VARS']['SYS']['productionExceptionHandler'], 'Debug') ||
257
258
259
260
261
262
263
264
265
266
267
268
            (Environment::getContext()->isProduction() && (int)$GLOBALS['TYPO3_CONF_VARS']['SYS']['displayErrors'] === 1)
        ) {
            $value = $this->getLanguageService()->getLL('status_insecure');
            $severity = ReportStatus::ERROR;
            $message = $this->getLanguageService()->getLL('status_exceptionHandler_errorMessage');
        } elseif ((int)$GLOBALS['TYPO3_CONF_VARS']['SYS']['displayErrors'] === 1) {
            $severity = ReportStatus::WARNING;
            $message = $this->getLanguageService()->getLL('status_exceptionHandler_warningMessage');
        }
        return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_exceptionHandler'), $value, $message, $severity);
    }

269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
    protected function getExportedFilesStatus(): ReportStatus
    {
        $value = $this->getLanguageService()->getLL('status_ok');
        $message = '';
        $severity = ReportStatus::OK;

        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file');
        $exportedFiles = $queryBuilder
            ->select('storage', 'identifier')
            ->from('sys_file')
            ->where(
                $queryBuilder->expr()->like(
                    'identifier',
                    $queryBuilder->createNamedParameter('%/_temp_/importexport/%')
                ),
                $queryBuilder->expr()->or(
                    $queryBuilder->expr()->like(
                        'identifier',
                        $queryBuilder->createNamedParameter('%.xml')
                    ),
                    $queryBuilder->expr()->like(
                        'identifier',
                        $queryBuilder->createNamedParameter('%.t3d')
                    )
                ),
            )
            ->executeQuery()
            ->fetchAllAssociative();

        if (count($exportedFiles) > 0) {
            $files = [];
            foreach ($exportedFiles as $exportedFile) {
                $files[] = '<li>' . htmlspecialchars($exportedFile['storage'] . ':' . $exportedFile['identifier']) . '</li>';
            }

            $value = $this->getLanguageService()->getLL('status_insecure');
            $severity = ReportStatus::WARNING;
            $message = $this->getLanguageService()->getLL('status_exportedFiles_warningMessage');
            $message .= '<ul>' . implode(PHP_EOL, $files) . '</ul>';
            $message .= $this->getLanguageService()->getLL('status_exportedFiles_warningRecommendation');
        }

        return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_exportedFiles'), $value, $message, $severity);
    }

314
    protected function getLanguageService(): LanguageService
315
316
317
    {
        return $GLOBALS['LANG'];
    }
318
}