[FEATURE] Add HTTPS security check to reports module
[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 }
59
60 return $statuses;
61 }
62
63 /**
64 * Checks if the current connection is encrypted (HTTPS)
65 *
66 * @param ServerRequestInterface $request
67 * @return ReportStatus
68 */
69 protected function getEncryptedConnectionStatus(ServerRequestInterface $request): ReportStatus
70 {
71 $value = $this->getLanguageService()->getLL('status_ok');
72 $message = '';
73 $severity = ReportStatus::OK;
74
75 /** @var \TYPO3\CMS\Core\Http\NormalizedParams $normalizedParams */
76 $normalizedParams = $request->getAttribute('normalizedParams');
77
78 if (!$normalizedParams->isHttps()) {
79 $value = $this->getLanguageService()->getLL('status_insecure');
80 $severity = ReportStatus::WARNING;
81 $message = $this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_encryptedConnectionStatus_insecure');
82 }
83
84 return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_encryptedConnectionStatus'), $value, $message, $severity);
85 }
86
87 /**
88 * Checks if the trusted hosts pattern check is disabled.
89 *
90 * @return ReportStatus An object representing whether the check is disabled
91 */
92 protected function getTrustedHostsPatternStatus(): ReportStatus
93 {
94 $value = $this->getLanguageService()->getLL('status_ok');
95 $message = '';
96 $severity = ReportStatus::OK;
97
98 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] === GeneralUtility::ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL) {
99 $value = $this->getLanguageService()->getLL('status_insecure');
100 $severity = ReportStatus::ERROR;
101 $message = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:warning.install_trustedhosts');
102 }
103
104 return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_trustedHostsPattern'), $value, $message, $severity);
105 }
106
107 /**
108 * Checks whether a BE user account named admin with default password exists.
109 *
110 * @return ReportStatus An object representing whether a default admin account exists
111 */
112 protected function getAdminAccountStatus(): ReportStatus
113 {
114 $value = $this->getLanguageService()->getLL('status_ok');
115 $message = '';
116 $severity = ReportStatus::OK;
117
118 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
119 $queryBuilder->getRestrictions()
120 ->removeAll()
121 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
122
123 $row = $queryBuilder
124 ->select('uid', 'username', 'password')
125 ->from('be_users')
126 ->where(
127 $queryBuilder->expr()->eq(
128 'username',
129 $queryBuilder->createNamedParameter('admin', \PDO::PARAM_STR)
130 )
131 )
132 ->execute()
133 ->fetch();
134
135 if (!empty($row)) {
136 $secure = true;
137 /** @var \TYPO3\CMS\Saltedpasswords\Salt\SaltInterface $saltingObject */
138 $saltingObject = SaltFactory::getSaltingInstance($row['password']);
139 if (is_object($saltingObject)) {
140 if ($saltingObject->checkPassword('password', $row['password'])) {
141 $secure = false;
142 }
143 }
144 // Check against plain MD5
145 if ($row['password'] === '5f4dcc3b5aa765d61d8327deb882cf99') {
146 $secure = false;
147 }
148 if (!$secure) {
149 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
150 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
151 $value = $this->getLanguageService()->getLL('status_insecure');
152 $severity = ReportStatus::ERROR;
153 $editUserAccountUrl = (string)$uriBuilder->buildUriFromRoute(
154 'record_edit',
155 [
156 'edit[be_users][' . $row['uid'] . ']' => 'edit',
157 'returnUrl' => (string)$uriBuilder->buildUriFromRoute('system_reports')
158 ]
159 );
160 $message = sprintf(
161 $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:warning.backend_admin'),
162 '<a href="' . htmlspecialchars($editUserAccountUrl) . '">',
163 '</a>'
164 );
165 }
166 }
167
168 return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_adminUserAccount'), $value, $message, $severity);
169 }
170
171 /**
172 * Checks if fileDenyPattern was changed which is dangerous on Apache
173 *
174 * @return ReportStatus An object representing whether the file deny pattern has changed
175 */
176 protected function getFileDenyPatternStatus(): ReportStatus
177 {
178 $value = $this->getLanguageService()->getLL('status_ok');
179 $message = '';
180 $severity = ReportStatus::OK;
181 $defaultParts = GeneralUtility::trimExplode('|', FILE_DENY_PATTERN_DEFAULT, true);
182 $givenParts = GeneralUtility::trimExplode('|', $GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'], true);
183 $result = array_intersect($defaultParts, $givenParts);
184
185 if ($defaultParts !== $result) {
186 $value = $this->getLanguageService()->getLL('status_insecure');
187 $severity = ReportStatus::ERROR;
188 $message = sprintf(
189 $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:warning.file_deny_pattern_partsNotPresent'),
190 '<br /><pre>' . htmlspecialchars(FILE_DENY_PATTERN_DEFAULT) . '</pre><br />'
191 );
192 }
193
194 return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_fileDenyPattern'), $value, $message, $severity);
195 }
196
197 /**
198 * Checks if fileDenyPattern allows to upload .htaccess files which is
199 * dangerous on Apache.
200 *
201 * @return ReportStatus An object representing whether it's possible to upload .htaccess files
202 */
203 protected function getHtaccessUploadStatus(): ReportStatus
204 {
205 $value = $this->getLanguageService()->getLL('status_ok');
206 $message = '';
207 $severity = ReportStatus::OK;
208
209 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'] != FILE_DENY_PATTERN_DEFAULT
210 && GeneralUtility::verifyFilenameAgainstDenyPattern('.htaccess')) {
211 $value = $this->getLanguageService()->getLL('status_insecure');
212 $severity = ReportStatus::ERROR;
213 $message = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:warning.file_deny_htaccess');
214 }
215
216 return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_htaccessUploadProtection'), $value, $message, $severity);
217 }
218
219 /**
220 * Checks whether salted Passwords are configured or not.
221 *
222 * @return ReportStatus An object representing the security of the saltedpassswords extension
223 */
224 protected function getSaltedPasswordsStatus(): ReportStatus
225 {
226 $value = $this->getLanguageService()->getLL('status_ok');
227 $severity = ReportStatus::OK;
228 /** @var ExtensionManagerConfigurationUtility $configCheck */
229 $configCheck = GeneralUtility::makeInstance(ExtensionManagerConfigurationUtility::class);
230 $message = '<p>' . $this->getLanguageService()->getLL('status_saltedPasswords_infoText') . '</p>';
231 $messageDetail = '';
232 $resultCheck = $configCheck->checkConfigurationBackend([]);
233
234 switch ($resultCheck['errorType']) {
235 case FlashMessage::INFO:
236 $messageDetail .= $resultCheck['html'];
237 break;
238 case FlashMessage::WARNING:
239 $severity = ReportStatus::WARNING;
240 $messageDetail .= $resultCheck['html'];
241 break;
242 case FlashMessage::ERROR:
243 $value = $this->getLanguageService()->getLL('status_insecure');
244 $severity = ReportStatus::ERROR;
245 $messageDetail .= $resultCheck['html'];
246 break;
247 default:
248 }
249
250 $unsecureUserCount = SaltedPasswordsUtility::getNumberOfBackendUsersWithInsecurePassword();
251
252 if ($unsecureUserCount > 0) {
253 $value = $this->getLanguageService()->getLL('status_insecure');
254 $severity = ReportStatus::ERROR;
255 $messageDetail .= '<div class="panel panel-warning">' .
256 '<div class="panel-body">' .
257 $this->getLanguageService()->getLL('status_saltedPasswords_notAllPasswordsHashed') .
258 '</div>' .
259 '</div>';
260 }
261
262 $message .= $messageDetail;
263
264 if (empty($messageDetail)) {
265 $message = '';
266 }
267
268 return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_saltedPasswords'), $value, $message, $severity);
269 }
270
271 /**
272 * @return LanguageService
273 */
274 protected function getLanguageService(): LanguageService
275 {
276 return $GLOBALS['LANG'];
277 }
278 }