e5d1036906ec3c365525fb6d7b9f7ae0f8c37ebb
[Packages/TYPO3.CMS.git] / typo3 / sysext / reports / Classes / Report / Status / SecurityStatus.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Reports\Report\Status;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use Psr\Http\Message\ServerRequestInterface;
19 use TYPO3\CMS\Core\Database\ConnectionPool;
20 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
21 use TYPO3\CMS\Core\Localization\LanguageService;
22 use TYPO3\CMS\Core\Messaging\FlashMessage;
23 use TYPO3\CMS\Core\Utility\GeneralUtility;
24 use TYPO3\CMS\Reports\RequestAwareStatusProviderInterface;
25 use TYPO3\CMS\Reports\Status as ReportStatus;
26 use TYPO3\CMS\Saltedpasswords\Salt\SaltFactory;
27 use TYPO3\CMS\Saltedpasswords\Utility\ExtensionManagerConfigurationUtility;
28 use TYPO3\CMS\Saltedpasswords\Utility\SaltedPasswordsUtility;
29
30 /**
31 * Performs several checks about the system's health
32 */
33 class SecurityStatus implements RequestAwareStatusProviderInterface
34 {
35 /**
36 * @var ServerRequestInterface
37 */
38 protected $request;
39
40 /**
41 * Determines the security of this TYPO3 installation
42 *
43 * @param ServerRequestInterface|null $request
44 * @return ReportStatus[] List of statuses
45 */
46 public function getStatus(ServerRequestInterface $request = null)
47 {
48 $statuses = [
49 'trustedHostsPattern' => $this->getTrustedHostsPatternStatus(),
50 'adminUserAccount' => $this->getAdminAccountStatus(),
51 'fileDenyPattern' => $this->getFileDenyPatternStatus(),
52 'htaccessUpload' => $this->getHtaccessUploadStatus(),
53 'saltedpasswords' => $this->getSaltedPasswordsStatus(),
54 ];
55
56 if ($request !== null) {
57 $statuses['encryptedConnectionStatus'] = $this->getEncryptedConnectionStatus($request);
58 $lockSslStatus = $this->getLockSslStatus($request);
59 if ($lockSslStatus) {
60 $statuses['getLockSslStatus'] = $lockSslStatus;
61 }
62 }
63
64 return $statuses;
65 }
66
67 /**
68 * Checks if the current connection is encrypted (HTTPS)
69 *
70 * @param ServerRequestInterface $request
71 * @return ReportStatus
72 */
73 protected function getEncryptedConnectionStatus(ServerRequestInterface $request): ReportStatus
74 {
75 $value = $this->getLanguageService()->getLL('status_ok');
76 $message = '';
77 $severity = ReportStatus::OK;
78
79 /** @var \TYPO3\CMS\Core\Http\NormalizedParams $normalizedParams */
80 $normalizedParams = $request->getAttribute('normalizedParams');
81
82 if (!$normalizedParams->isHttps()) {
83 $value = $this->getLanguageService()->getLL('status_insecure');
84 $severity = ReportStatus::WARNING;
85 $message = $this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_encryptedConnectionStatus_insecure');
86 }
87
88 return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_encryptedConnectionStatus'), $value, $message, $severity);
89 }
90
91 /**
92 * @param ServerRequestInterface $request
93 * @return ReportStatus
94 */
95 protected function getLockSslStatus(ServerRequestInterface $request): ?ReportStatus
96 {
97 /** @var \TYPO3\CMS\Core\Http\NormalizedParams $normalizedParams */
98 $normalizedParams = $request->getAttribute('normalizedParams');
99
100 if ($normalizedParams->isHttps()) {
101 $value = $this->getLanguageService()->getLL('status_ok');
102 $message = '';
103 $severity = ReportStatus::OK;
104
105 if (!$GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSL']) {
106 $value = $this->getLanguageService()->getLL('status_insecure');
107 $message = $this->getLanguageService()->getLL('status_lockSslStatus_insecure');
108 $severity = ReportStatus::WARNING;
109 }
110
111 return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_lockSslStatus'), $value, $message, $severity);
112 }
113
114 return null;
115 }
116
117 /**
118 * Checks if the trusted hosts pattern check is disabled.
119 *
120 * @return ReportStatus An object representing whether the check is disabled
121 */
122 protected function getTrustedHostsPatternStatus(): ReportStatus
123 {
124 $value = $this->getLanguageService()->getLL('status_ok');
125 $message = '';
126 $severity = ReportStatus::OK;
127
128 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] === GeneralUtility::ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL) {
129 $value = $this->getLanguageService()->getLL('status_insecure');
130 $severity = ReportStatus::ERROR;
131 $message = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.install_trustedhosts');
132 }
133
134 return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_trustedHostsPattern'), $value, $message, $severity);
135 }
136
137 /**
138 * Checks whether a BE user account named admin with default password exists.
139 *
140 * @return ReportStatus An object representing whether a default admin account exists
141 */
142 protected function getAdminAccountStatus(): ReportStatus
143 {
144 $value = $this->getLanguageService()->getLL('status_ok');
145 $message = '';
146 $severity = ReportStatus::OK;
147
148 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
149 $queryBuilder->getRestrictions()
150 ->removeAll()
151 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
152
153 $row = $queryBuilder
154 ->select('uid', 'username', 'password')
155 ->from('be_users')
156 ->where(
157 $queryBuilder->expr()->eq(
158 'username',
159 $queryBuilder->createNamedParameter('admin', \PDO::PARAM_STR)
160 )
161 )
162 ->execute()
163 ->fetch();
164
165 if (!empty($row)) {
166 $secure = true;
167 /** @var \TYPO3\CMS\Saltedpasswords\Salt\SaltInterface $saltingObject */
168 $saltingObject = SaltFactory::getSaltingInstance($row['password']);
169 if (is_object($saltingObject)) {
170 if ($saltingObject->checkPassword('password', $row['password'])) {
171 $secure = false;
172 }
173 }
174 // Check against plain MD5
175 if ($row['password'] === '5f4dcc3b5aa765d61d8327deb882cf99') {
176 $secure = false;
177 }
178 if (!$secure) {
179 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
180 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
181 $value = $this->getLanguageService()->getLL('status_insecure');
182 $severity = ReportStatus::ERROR;
183 $editUserAccountUrl = (string)$uriBuilder->buildUriFromRoute(
184 'record_edit',
185 [
186 'edit[be_users][' . $row['uid'] . ']' => 'edit',
187 'returnUrl' => (string)$uriBuilder->buildUriFromRoute('system_reports')
188 ]
189 );
190 $message = sprintf(
191 $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.backend_admin'),
192 '<a href="' . htmlspecialchars($editUserAccountUrl) . '">',
193 '</a>'
194 );
195 }
196 }
197
198 return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_adminUserAccount'), $value, $message, $severity);
199 }
200
201 /**
202 * Checks if fileDenyPattern was changed which is dangerous on Apache
203 *
204 * @return ReportStatus An object representing whether the file deny pattern has changed
205 */
206 protected function getFileDenyPatternStatus(): ReportStatus
207 {
208 $value = $this->getLanguageService()->getLL('status_ok');
209 $message = '';
210 $severity = ReportStatus::OK;
211 $defaultParts = GeneralUtility::trimExplode('|', FILE_DENY_PATTERN_DEFAULT, true);
212 $givenParts = GeneralUtility::trimExplode('|', $GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'], true);
213 $result = array_intersect($defaultParts, $givenParts);
214
215 if ($defaultParts !== $result) {
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(FILE_DENY_PATTERN_DEFAULT) . '</pre><br />'
221 );
222 }
223
224 return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_fileDenyPattern'), $value, $message, $severity);
225 }
226
227 /**
228 * Checks if fileDenyPattern allows to upload .htaccess files which is
229 * dangerous on Apache.
230 *
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 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'] != FILE_DENY_PATTERN_DEFAULT
240 && GeneralUtility::verifyFilenameAgainstDenyPattern('.htaccess')) {
241 $value = $this->getLanguageService()->getLL('status_insecure');
242 $severity = ReportStatus::ERROR;
243 $message = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_deny_htaccess');
244 }
245
246 return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_htaccessUploadProtection'), $value, $message, $severity);
247 }
248
249 /**
250 * Checks whether salted Passwords are configured or not.
251 *
252 * @return ReportStatus An object representing the security of the saltedpassswords extension
253 */
254 protected function getSaltedPasswordsStatus(): ReportStatus
255 {
256 $value = $this->getLanguageService()->getLL('status_ok');
257 $severity = ReportStatus::OK;
258 /** @var ExtensionManagerConfigurationUtility $configCheck */
259 $configCheck = GeneralUtility::makeInstance(ExtensionManagerConfigurationUtility::class);
260 $message = '<p>' . $this->getLanguageService()->getLL('status_saltedPasswords_infoText') . '</p>';
261 $messageDetail = '';
262 $resultCheck = $configCheck->checkConfigurationBackend([]);
263
264 switch ($resultCheck['errorType']) {
265 case FlashMessage::INFO:
266 $messageDetail .= $resultCheck['html'];
267 break;
268 case FlashMessage::WARNING:
269 $severity = ReportStatus::WARNING;
270 $messageDetail .= $resultCheck['html'];
271 break;
272 case FlashMessage::ERROR:
273 $value = $this->getLanguageService()->getLL('status_insecure');
274 $severity = ReportStatus::ERROR;
275 $messageDetail .= $resultCheck['html'];
276 break;
277 default:
278 }
279
280 $unsecureUserCount = SaltedPasswordsUtility::getNumberOfBackendUsersWithInsecurePassword();
281
282 if ($unsecureUserCount > 0) {
283 $value = $this->getLanguageService()->getLL('status_insecure');
284 $severity = ReportStatus::ERROR;
285 $messageDetail .= '<div class="panel panel-warning">' .
286 '<div class="panel-body">' .
287 $this->getLanguageService()->getLL('status_saltedPasswords_notAllPasswordsHashed') .
288 '</div>' .
289 '</div>';
290 }
291
292 $message .= $messageDetail;
293
294 if (empty($messageDetail)) {
295 $message = '';
296 }
297
298 return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_saltedPasswords'), $value, $message, $severity);
299 }
300
301 /**
302 * @return LanguageService
303 */
304 protected function getLanguageService(): LanguageService
305 {
306 return $GLOBALS['LANG'];
307 }
308 }