Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Moving several important warnings to templates #8422

Merged
merged 4 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Languages/en_US/General.php
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@
$txt['search_advanced'] = 'Advanced search';

$txt['security_risk'] = 'MAJOR SECURITY RISK:';
$txt['not_removed'] = 'You have not removed ';
$txt['not_removed'] = 'You have not removed {filename}.';
$txt['not_removed_extra'] = '{backup_filename} is a backup of {filename} that was not generated by SMF. It can be accessed directly and used to gain unauthorized access to your forum. You should delete it immediately.';
$txt['generic_warning'] = 'Warning';
$txt['agreement_missing'] = 'You are requiring new users to accept a registration agreement; however, the file (agreement.txt) does not exist.';
Expand Down
6 changes: 3 additions & 3 deletions Sources/Lang.php
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ public static function get(bool $use_cache = true): array
* array will be used as a sub-key to drill down into deeper levels of
* the overall array.
* @param array $args Arguments to substitute into the Lang::$txt string.
* @param array $var Name of the array to search in. Default: 'txt'.
* @param string $var Name of the array to search in. Default: 'txt'.
* Other possible values are 'helptxt', 'editortxt', and 'tztxt'.
* @return string The string to display to the user.
*/
Expand Down Expand Up @@ -742,8 +742,8 @@ public static function sentenceList(array $list, string $type = 'and'): string
* current locale to format the number.
*
* @param int|float|string $number A number.
* @param int $decimals If set, will use the specified number of decimal
* places. Otherwise it's automatically determined.
* @param int|null $decimals If set, will use the specified number of decimal
* places. Otherwise, it's automatically determined.
* @return string A formatted number
*/
public static function numberFormat(int|float|string $number, ?int $decimals = null): string
Expand Down
89 changes: 89 additions & 0 deletions Sources/Security.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

namespace SMF;

use SMF\Cache\CacheApi;
use SMF\Db\DatabaseApi as Db;

/**
Expand Down Expand Up @@ -238,6 +239,94 @@ public static function spamProtection(string $error_type, bool $only_return_resu
return false;
}

/**
* Checks for the existence and security status of specific files and directories
* required for the proper functioning of the system. Ensures that security measures
* are applied and generates a list of warnings for any issues detected.
*
* Warnings include:
* - Missing or insecure critical files (e.g., install.php, upgrade.php).
* - Directories not properly secured (e.g., cache directory).
* - Missing legal agreement or policy documents.
* - Missing authentication secrets.
*
* @return bool|array Returns an array of warnings if any security risks are detected,
* or false if the user lacks the necessary permissions or is a guest.
*/
public static function checkSecurityFiles(): bool|array
{
if (User::$me->allowedTo('admin_forum') && !User::$me->is_guest) {
$security_files = [
'install.php',
'upgrade.php',
'convert.php',
'repair_paths.php',
'repair_settings.php',
'Settings.php~',
'Settings_bak.php~',
];

// Add your own files.
IntegrationHook::call('integrate_security_files', [&$security_files]);

// Filter out missing security files
$security_files = array_filter($security_files, function ($file) {
return file_exists(Config::$boarddir . '/' . $file);
});

// Determine attachment upload directory path
$path = !empty(Config::$modSettings['currentAttachmentUploadDir'])
? Config::$modSettings['attachmentUploadDir'][Config::$modSettings['currentAttachmentUploadDir']]
: Config::$modSettings['attachmentUploadDir'];
live627 marked this conversation as resolved.
Show resolved Hide resolved

// Secure directories
self::secureDirectory($path, true);
self::secureDirectory(Config::$cachedir);

// Check for required files
$agreement = !empty(Config::$modSettings['requireAgreement'])
&& !file_exists(Config::$languagesdir . '/en_US/agreement.txt');
$policy_agreement = !empty(Config::$modSettings['requirePolicyAgreement'])
&& empty(Config::$modSettings['policy_' . Lang::$default]);
live627 marked this conversation as resolved.
Show resolved Hide resolved

// Compile warnings
$warnings = [];

foreach ($security_files as $security_file) {
$warnings['file'][] = ['not_removed', [
'filename' => $security_file,
]];

if (in_array($security_file, ['Settings.php~', 'Settings_bak.php~'])) {
$warnings['file'][] = ['not_removed_extra', [
'backup_filename' => $security_file,
'filename' => substr($security_file, 0, -1),
]];
}
}

if (!empty(CacheApi::$enable) && !is_writable(Config::$cachedir)) {
$warnings[] = ['cache_writable'];
}

if ($agreement) {
$warnings[] = ['agreement_missing'];
}

if ($policy_agreement) {
$warnings[] = ['policy_agreement_missing'];
}

if (!empty(Utils::$context['auth_secret_missing'])) {
$warnings[] = ['auth_secret_missing'];
}

return $warnings;
}

return false;
}

/**
* A generic function to create a pair of index.php and .htaccess files in a directory
*
Expand Down
135 changes: 21 additions & 114 deletions Sources/Theme.php
Original file line number Diff line number Diff line change
Expand Up @@ -1295,129 +1295,36 @@ public static function template_header(): void

header('Content-Type: ' . $content_type . '; charset=' . (empty(Utils::$context['character_set']) ? 'ISO-8859-1' : Utils::$context['character_set']));

// We need to splice this in after the body layer.
// Collect layers to be added
$layers = [];

// Add maintenance warning if in maintenance mode and user is admin
if (Utils::$context['in_maintenance'] && User::$me->is_admin) {
$layers[] = 'maint_warning';
}

// Add security warning if security issues are detected
Utils::$context['warnings'] = Security::checkSecurityFiles();
if (Utils::$context['warnings']) {
$layers[] = 'security_warning';
}

// Inform user if they are banned from posting
if (isset($_SESSION['ban']['cannot_post'])) {
$layers[] = 'banned_warning';
}

// Insert all collected layers after the 'body' layer
if ($layers != []) {
$position = array_search('body', Utils::$context['template_layers']);

if ($position !== false) {
array_splice(Utils::$context['template_layers'], $position + 1, 0, ['maint_warning']);
array_splice(Utils::$context['template_layers'], $position + 1, 0, $layers);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this code be better suited for Theme:::loadTemplatesAndLangFiles()?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would fit better there in an abstract, conceptual sense, yes. But then it would be called during Theme::load(), which would be problematic for a number of reasons.

}
}

$checked_securityFiles = false;
$showed_banned = false;

foreach (Utils::$context['template_layers'] as $layer) {
self::loadSubTemplate($layer . '_above', true);

// May seem contrived, but this is done in case the body and main layer aren't there...
if (in_array($layer, ['body', 'main']) && User::$me->allowedTo('admin_forum') && !User::$me->is_guest && !$checked_securityFiles) {
$checked_securityFiles = true;

$securityFiles = ['install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~'];

// Add your own files.
IntegrationHook::call('integrate_security_files', [&$securityFiles]);

foreach ($securityFiles as $i => $securityFile) {
if (!file_exists(Config::$boarddir . '/' . $securityFile)) {
unset($securityFiles[$i]);
}
}

// We are already checking so many files...just few more doesn't make any difference! :P
if (!empty(Config::$modSettings['currentAttachmentUploadDir'])) {
$path = Config::$modSettings['attachmentUploadDir'][Config::$modSettings['currentAttachmentUploadDir']];
} else {
$path = Config::$modSettings['attachmentUploadDir'];
}

Security::secureDirectory($path, true);
Security::secureDirectory(Config::$cachedir);

// If agreement is enabled, at least the english version shall exist
if (!empty(Config::$modSettings['requireAgreement'])) {
$agreement = !file_exists(Config::$languagesdir . '/en_US/agreement.txt');
}

// If privacy policy is enabled, at least the default language version shall exist
if (!empty(Config::$modSettings['requirePolicyAgreement'])) {
$policy_agreement = empty(Config::$modSettings['policy_' . Lang::$default]);
}

if (
!empty($securityFiles)
|| (
!empty(CacheApi::$enable)
&& !is_writable(Config::$cachedir)
)
|| !empty($agreement)
|| !empty($policy_agreement)
|| !empty(Utils::$context['auth_secret_missing'])) {
echo '
<div class="errorbox">
<p class="alert">!!</p>
<h3>', empty($securityFiles) && empty(Utils::$context['auth_secret_missing']) ? Lang::$txt['generic_warning'] : Lang::$txt['security_risk'], '</h3>
<p>';

foreach ($securityFiles as $securityFile) {
echo '
', Lang::$txt['not_removed'], '<strong>', $securityFile, '</strong>!<br>';

if ($securityFile == 'Settings.php~' || $securityFile == 'Settings_bak.php~') {
echo '
', Lang::getTxt('not_removed_extra', ['backup_filename' => $securityFile, 'filename' => substr($securityFile, 0, -1)]), '<br>';
}
}

if (!empty(CacheApi::$enable) && !is_writable(Config::$cachedir)) {
echo '
<strong>', Lang::$txt['cache_writable'], '</strong><br>';
}

if (!empty($agreement)) {
echo '
<strong>', Lang::$txt['agreement_missing'], '</strong><br>';
}

if (!empty($policy_agreement)) {
echo '
<strong>', Lang::$txt['policy_agreement_missing'], '</strong><br>';
}

if (!empty(Utils::$context['auth_secret_missing'])) {
echo '
<strong>', Lang::$txt['auth_secret_missing'], '</strong><br>';
}

echo '
</p>
</div>';
}
}
// If the user is banned from posting inform them of it.
elseif (in_array($layer, ['main', 'body']) && isset($_SESSION['ban']['cannot_post']) && !$showed_banned) {
$showed_banned = true;
echo '
<div class="windowbg alert" style="margin: 2ex; padding: 2ex; border: 2px dashed red;">
', Lang::getTxt('you_are_post_banned', ['name' => User::$me->is_guest ? Lang::$txt['guest_title'] : User::$me->name]);

if (!empty($_SESSION['ban']['cannot_post']['reason'])) {
echo '
<div style="padding-left: 4ex; padding-top: 1ex;">', $_SESSION['ban']['cannot_post']['reason'], '</div>';
}

if (!empty($_SESSION['ban']['expire_time'])) {
echo '
<div>', Lang::getTxt('your_ban_expires', ['datetime' => Time::create('@' . $_SESSION['ban']['expire_time'])->format(null, false)]), '</div>';
} else {
echo '
<div>', Lang::$txt['your_ban_expires_never'], '</div>';
}

echo '
</div>';
}
}
}

Expand Down
75 changes: 75 additions & 0 deletions Themes/default/index.template.php
Original file line number Diff line number Diff line change
Expand Up @@ -807,4 +807,79 @@ function template_maint_warning_below()

}

/**
* The upper part of the security warning box
*/
function template_security_warning_above()
{
echo '
<div class="errorbox">
<p class="alert">!!</p>
<h3>', !isset(Utils::$context['warnings']['file']) && empty(Utils::$context['auth_secret_missing'])
? Lang::$txt['generic_warning']
: Lang::$txt['security_risk'], '</h3>';

foreach (Utils::$context['warnings']['file'] as $security_file) {
echo '
<p>', Lang::getTxt($security_file[0], $security_file[1]), '</p>';
}

for ($i = 0, $n = count(Utils::$context['warnings']) - 1; $i < $n; $i++) {
if (is_string(Utils::$context['warnings'][$i])) {
echo '
<p>' . Utils::$context['warnings'][$i] . '</p>';
} else {
echo '
<p>', Lang::getTxt(Utils::$context['warnings'][$i][0], Utils::$context['warnings'][$i][1] ?? []), '</p>';
}
}

echo '
</div>';
}

/**
* The lower part of the security warning box.
*/
function template_security_warning_below()
{

}

/**
* The upper part of the ban warning box
*/
function template_banned_warning_above()
{
echo '
<div class="noticebox">';

echo '
<p>', Lang::getTxt('you_are_post_banned', ['name' => User::$me->is_guest ? Lang::$txt['guest_title'] : User::$me->name]), '</p>';

if (!empty($_SESSION['ban']['cannot_post']['reason'])) {
echo '
<p>', $_SESSION['ban']['cannot_post']['reason'], '</p>';
}

if (!empty($_SESSION['ban']['expire_time'])) {
echo '
<p>', Lang::getTxt('your_ban_expires', ['datetime' => Time::create('@' . $_SESSION['ban']['expire_time'])->format(null, false)]), '</p>';
} else {
echo '
<p>', Lang::$txt['your_ban_expires_never'], '</p>';
}

echo '
</div>';
}

/**
* The lower part of the ban warning box.
*/
function template_banned_warning_below()
{

}

?>