'" . ($config[$key] ?? "") . "'," . PHP_EOL; } $content .= "];" . PHP_EOL; // Write new config file file_put_contents($configPhp, $content); // Make old file unreadable chmod($configIni, 000); } /** * Iterates over all 30 custom vars, explode them to split between key and value, append to $config. * * @param array $config * @return array * @throws UserReportException */ private static function getCustomVariable(array $config) { for ($i = 1; $i <= 30; $i++) { if (isset($config['custom' . $i])) { $arr = explode('=', $config['custom' . $i], 2); if (!empty($arr[0]) && !empty($arr[1])) { $arr[0] = trim($arr[0]); $arr[1] = OnString::trimQuote(trim($arr[1])); if (isset($config[$arr[0]])) { throw new UserReportException("Variable '$arr[0]' already defined", ERROR_INVALID_OR_MISSING_PARAMETER); } $config[$arr[0]] = $arr[1]; } } } return $config; } /** * Read config.qfq.ini. In case * * @param string $fileConfigPhp * @return array * @throws CodeException * @throws UserFormException * @throws UserReportException */ public static function readConfig($fileConfigPhp = '') { $configT3qfq = array(); $configIni = ''; // outdated config file format // Production Path to CONFIG_INI $pathTypo3Conf = __DIR__ . '/../../../../..'; if (!file_exists($pathTypo3Conf . '/' . CONFIG_T3)) { // PHPUnit Path to CONFIG_INI $pathTypo3Conf = __DIR__ . '/../../..'; } // In case of missing $configPhp if (empty($fileConfigPhp)) { # Read 'LocalConfiguration.php' if (isset($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][EXT_KEY])) { $configT3qfq = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][EXT_KEY]); $configT3qfq[SYSTEM_DB_NAME_T3] = self::getDbName($GLOBALS['TYPO3_CONF_VARS']['DB']); } else { $all = include($pathTypo3Conf . '/' . CONFIG_T3); if (empty($all) || $all === true) { throw new qfq\UserFormException ("Error read file: " . $pathTypo3Conf . '/' . CONFIG_T3, ERROR_IO_READ_FILE); } $configT3qfq = unserialize($all['EXT']['extConf'][EXT_KEY]); if (!is_array($configT3qfq)) { throw new qfq\UserFormException ("Error read file: " . $pathTypo3Conf . '/' . CONFIG_T3, ERROR_IO_READ_FILE); } $configT3qfq[SYSTEM_DB_NAME_T3] = self::getDbName($all['DB']); unset($all); } $configIni = $pathTypo3Conf . '/' . CONFIG_QFQ_INI; $fileConfigPhp = $pathTypo3Conf . '/' . CONFIG_QFQ_PHP; } // Migrate legacy config file. if (is_readable($configIni) && !is_readable($fileConfigPhp)) { self::migrateConfigIniToPhp($configIni, $fileConfigPhp); } $config = include($fileConfigPhp); if ($config === false) { throw new qfq\UserFormException ("Error read file: " . $fileConfigPhp, ERROR_IO_READ_FILE); } // in case $configIni doesn't exist: just skip if (!is_array($config)) { $config = array(); } $configT3qfq = self::getCustomVariable($configT3qfq); // Settings in config.qfq.php overwrite T3 settings $config = array_merge($configT3qfq, $config); $config = self::renameConfigElements($config); $config = self::setDefaults($config); self::checkDeprecated($config); self::checkForAttack($config); // Copy values to detect custom settings later $config[F_FE_DATA_PATTERN_ERROR_SYSTEM] = $config[F_FE_DATA_PATTERN_ERROR]; return $config; } /** * Returns T3 DB-Name, depending on T3 version * * @param array $db * @return mixed */ private static function getDbName(array $db) { // T3 7.x: $GLOBALS['TYPO3_CONF_VARS']['DB']['database'], T3 8.x: $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname'] return isset($db['database']) ? $db['database'] : $db['Connections']['Default']['dbname']; } /** * Checks for deprecated options. * * @param array $config * @throws UserFormException */ private static function checkDeprecated(array $config) { foreach ([SYSTEM_VAR_ADD_BY_SQL] as $key) { if (isset($config[$key])) { $msg = ''; switch ($key) { case SYSTEM_VAR_ADD_BY_SQL: $msg = 'Replaced by: ' . SYSTEM_FILL_STORE_SYSTEM_BY_SQL . '1|2|3'; } throw new qfq\UserFormException ("Deprecated option in " . CONFIG_QFQ_PHP . ": " . SYSTEM_VAR_ADD_BY_SQL . " - " . $msg); } } } /** * Check for attack * * @param array $config * @throws CodeException * @throws UserFormException * @throws UserReportException */ public static function checkForAttack(array $config) { $attack = false; $key = ''; $reason = 'Problem: '; // Iterate over all fake vars $arr = explode(',', $config[SYSTEM_SECURITY_VARS_HONEYPOT]); foreach ($arr as $key) { $key = trim($key); if ($key === '') { continue; } if (!empty($_POST[$key])) { $attack = true; $reason .= "Post/Get Honeypot variable '$key' detected: " . htmlentities($_POST[$key]) . PHP_EOL; } } // Limit length of all get vars: protect against SQL injection based on long ...%34%34%24%34... $maxLength = $config[SYSTEM_SECURITY_GET_MAX_LENGTH]; if ($maxLength > 0 && $attack === false) { foreach ($_GET as $key => $value) { if (!is_string($value)) { continue; } // Check if the variable is something like 'my_name_100' - if the part after the last '_' is numerical, this means a valid, non standard length. $arr = explode(GET_EXTRA_LENGTH_TOKEN, $key); $cnt = count($arr); if ($cnt > 1 && is_numeric($arr[$cnt - 1])) { $maxLength = $arr[$cnt - 1]; } else { $maxLength = $config[SYSTEM_SECURITY_GET_MAX_LENGTH]; // might change again. } $len = strlen($value); if ($len > $maxLength) { $attack = true; $reason .= "Value of GET variable '$key' too long. Allowed: $maxLength, Length: $len. Value: '" . htmlentities($_GET[$key]) . "'" . PHP_EOL; } } } // Nothing found? if ($attack === false) { return; } self::attackDetectedExitNow($config, $reason); } /** * @param array $config * @param string $reason * @throws CodeException * @throws UserFormException * @throws UserReportException */ public static function attackDetectedExitNow(array $config = array(), $reason = '') { if (count($config) == 0) { $config = self::readConfig(); } Logger::logMessage(Logger::linePre() . 'Security: attack detected' . PHP_EOL . $reason, $config[SYSTEM_QFQ_LOG] ?? SYSTEM_QFQ_LOG_FILE); // In case of an attack: log out the current user. Session::destroy(); // Sleep $penalty = (empty($config[SYSTEM_SECURITY_ATTACK_DELAY]) || !is_numeric($config[SYSTEM_SECURITY_ATTACK_DELAY])) ? SYSTEM_SECURITY_ATTACK_DELAY_DEFAULT : $config[SYSTEM_SECURITY_ATTACK_DELAY]; if (!defined('PHPUNIT_QFQ')) { sleep($penalty); } if ($config[SYSTEM_SECURITY_SHOW_MESSAGE] == 'true' || $config[SYSTEM_SECURITY_SHOW_MESSAGE] == 1) { echo "Attack detected - stop process

" . $reason . '

'; // $answer[API_STATUS] = API_ANSWER_STATUS_ERROR; // $answer[API_MESSAGE] = 'Attack detected - stop process.'; // if($getParamName!='') { // $answer[API_MESSAGE] .= " Attack parameter: $getParamName"; // } // header("Content-Type: application/json"); // echo json_encode($answer); } if (defined('PHPUNIT_QFQ')) { throw new UserFormException('Attack detected', 1); } exit; } /** * @param array $config * * @return array */ public static function setDefaults(array $config) { $default = [ SYSTEM_DB_INIT => 'set names utf8', SYSTEM_DB_INDEX_DATA => DB_INDEX_DEFAULT, SYSTEM_DB_INDEX_QFQ => DB_INDEX_DEFAULT, SYSTEM_DATE_FORMAT => 'yyyy-mm-dd', SYSTEM_SHOW_DEBUG_INFO => SYSTEM_SHOW_DEBUG_INFO_AUTO, SYSTEM_MAIL_LOG => SYSTEM_MAIL_LOG_FILE, SYSTEM_QFQ_LOG => SYSTEM_QFQ_LOG_FILE, SYSTEM_SQL_LOG => SYSTEM_SQL_LOG_FILE, SYSTEM_SQL_LOG_MODE => 'modify', F_BS_COLUMNS => 'col-md-12 col-lg-10', F_BS_LABEL_COLUMNS => 'col-md-3 col-lg-3', F_BS_INPUT_COLUMNS => 'col-md-6 col-lg-6', F_BS_NOTE_COLUMNS => 'col-md-3 col-lg-3', SYSTEM_CMD_WKHTMLTOPDF => '/opt/wkhtmltox/bin/wkhtmltopdf', F_CLASS_PILL => 'qfq-color-grey-1', F_CLASS_BODY => 'qfq-color-grey-2', F_SAVE_BUTTON_TEXT => '', F_SAVE_BUTTON_TOOLTIP => '', F_SAVE_BUTTON_CLASS => 'btn btn-default navbar-btn', F_SAVE_BUTTON_GLYPH_ICON => GLYPH_ICON_CHECK, F_CLOSE_BUTTON_TEXT => '', F_CLOSE_BUTTON_TOOLTIP => 'Close', F_CLOSE_BUTTON_CLASS => 'btn btn-default navbar-btn', F_CLOSE_BUTTON_GLYPH_ICON => GLYPH_ICON_CLOSE, F_DELETE_BUTTON_TEXT => '', F_DELETE_BUTTON_TOOLTIP => 'Delete', F_DELETE_BUTTON_CLASS => 'btn btn-default navbar-btn', F_DELETE_BUTTON_GLYPH_ICON => GLYPH_ICON_DELETE, F_NEW_BUTTON_TEXT => '', F_NEW_BUTTON_TOOLTIP => 'New', F_NEW_BUTTON_CLASS => 'btn btn-default navbar-btn', F_NEW_BUTTON_GLYPH_ICON => GLYPH_ICON_NEW, F_BUTTON_ON_CHANGE_CLASS => 'btn-info alert-info', SYSTEM_EDIT_FORM_PAGE => 'form', SYSTEM_SECURITY_VARS_HONEYPOT => SYSTEM_SECURITY_VARS_HONEYPOT_NAMES, SYSTEM_SECURITY_ATTACK_DELAY => SYSTEM_SECURITY_ATTACK_DELAY_DEFAULT, SYSTEM_SECURITY_SHOW_MESSAGE => '0', SYSTEM_SECURITY_GET_MAX_LENGTH => SYSTEM_SECURITY_GET_MAX_LENGTH_DEFAULT, SYSTEM_LABEL_ALIGN => SYSTEM_LABEL_ALIGN_LEFT, SYSTEM_ESCAPE_TYPE_DEFAULT => TOKEN_ESCAPE_MYSQL, SYSTEM_EXTRA_BUTTON_INFO_INLINE => '', SYSTEM_EXTRA_BUTTON_INFO_BELOW => '', SYSTEM_EXTRA_BUTTON_INFO_CLASS => '', SYSTEM_DB_UPDATE => SYSTEM_DB_UPDATE_AUTO, SYSTEM_RECORD_LOCK_TIMEOUT_SECONDS => SYSTEM_RECORD_LOCK_TIMEOUT_SECONDS_DEFAULT, SYSTEM_SESSION_TIMEOUT_SECONDS => self::getPhpSessionTimeout(), SYSTEM_DOCUMENTATION_QFQ => SYSTEM_DOCUMENTATION_QFQ_URL, SYSTEM_ENTER_AS_SUBMIT => 1, SYSTEM_CMD_INKSCAPE => 'inkscape', SYSTEM_CMD_CONVERT => 'convert', SYSTEM_THUMBNAIL_DIR_SECURE => SYSTEM_THUMBNAIL_DIR_SECURE_DEFAULT, SYSTEM_THUMBNAIL_DIR_PUBLIC => SYSTEM_THUMBNAIL_DIR_PUBLIC_DEFAULT, F_FE_DATA_REQUIRED_ERROR => F_FE_DATA_REQUIRED_ERROR_DEFAULT, F_FE_DATA_MATCH_ERROR => F_FE_DATA_MATCH_ERROR_DEFAULT, F_FE_DATA_ERROR => F_FE_DATA_ERROR_DEFAULT, F_FE_DATA_PATTERN_ERROR => F_FE_DATA_PATTERN_ERROR_DEFAULT, SYSTEM_FLAG_PRODUCTION => 'yes', SYSTEM_THROW_GENERAL_ERROR => 'auto', SYSTEM_SECURITY_FAILED_AUTH_DELAY => '3', SYSTEM_FILE_MAX_FILE_SIZE => min(Support::returnBytes(ini_get('post_max_size')), Support::returnBytes(ini_get('upload_max_filesize'))), ]; foreach ($default as $key => $value) { if (!isset($config[$key]) || $config[$key] == '') { $config[$key] = $value; } } // don't accept deprecated documentation url if ($config[SYSTEM_DOCUMENTATION_QFQ] === 'https://docs.typo3.org/typo3cms/drafts/github/T3DocumentationStarter/Public-Info-053/Manual.html') { $config[SYSTEM_DOCUMENTATION_QFQ] = SYSTEM_DOCUMENTATION_QFQ_URL; } return $config; } /** * Rename Elements defined in config.qfq.ini to more appropriate in user interaction. * E.g.: in config.qfq.ini everything is in upper case and word space is '_'. In Form.parameter it's lowercase and * camel hook. * * @param array $config * * @return array */ private static function renameConfigElements(array $config) { // oldname > newname $setting = [ [SYSTEM_FORM_BS_COLUMNS, F_BS_COLUMNS], [SYSTEM_FORM_BS_LABEL_COLUMNS, F_BS_LABEL_COLUMNS], [SYSTEM_FORM_BS_INPUT_COLUMNS, F_BS_INPUT_COLUMNS], [SYSTEM_FORM_BS_NOTE_COLUMNS, F_BS_NOTE_COLUMNS], [SYSTEM_FORM_DATA_PATTERN_ERROR, F_FE_DATA_PATTERN_ERROR], [SYSTEM_FORM_DATA_REQUIRED_ERROR, F_FE_DATA_REQUIRED_ERROR], [SYSTEM_FORM_DATA_MATCH_ERROR, F_FE_DATA_MATCH_ERROR], [SYSTEM_FORM_DATA_ERROR, F_FE_DATA_ERROR], [SYSTEM_CSS_CLASS_QFQ_FORM, F_CLASS], [SYSTEM_CSS_CLASS_QFQ_FORM_PILL, F_CLASS_PILL], [SYSTEM_CSS_CLASS_QFQ_FORM_BODY, F_CLASS_BODY], [SYSTEM_SAVE_BUTTON_CLASS_ON_CHANGE, F_BUTTON_ON_CHANGE_CLASS], ]; foreach ($setting as $row) { $oldName = $row[0]; $newName = $row[1]; if (isset($config[$oldName])) { $config[$newName] = $config[$oldName]; if ($oldName != $newName) { unset($config[$oldName]); } } } return $config; } /** * Check Session Timeout * * @param $timeout * @throws UserFormException */ public static function checkSessionTimeout($timeout) { if (self::getPhpSessionTimeout() < $timeout) { throw new qfq\UserFormException ("The specified timeout of $timeout seconds is higher than the PHP config 'session.gc_maxlifetime' (" . ini_get('session.gc_maxlifetime') . ") and/or 'session.cookie_lifetime' (" . ini_get('session.cookie_lifetime') . ")"); } if ($timeout > SYSTEM_COOKIE_LIFETIME) { throw new qfq\UserFormException ("The specified timeout of $timeout seconds is higher than the hardcoded cookie lifetime (" . SYSTEM_COOKIE_LIFETIME . ")"); } } /** * Get the minimum of 'session.cookie_lifetime' and 'session.gc_maxlifetime' * A zero means unlimited and will be limited to one day. * * @return int|string */ private static function getPhpSessionTimeout() { $timeout = ini_get('session.cookie_lifetime'); $gc_maxlifetime = ini_get('session.gc_maxlifetime'); if ($timeout == 0) { $timeout = 86400; } if ($gc_maxlifetime == 0) { // Just to set a limit $timeout = 86400; } return $timeout > $gc_maxlifetime ? $gc_maxlifetime : $timeout; } }