. /** * Lib functions * * @package report * @subpackage security * @copyright 2008 petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; define('REPORT_SECURITY_OK', 'ok'); define('REPORT_SECURITY_INFO', 'info'); define('REPORT_SECURITY_WARNING', 'warning'); define('REPORT_SECURITY_SERIOUS', 'serious'); define('REPORT_SECURITY_CRITICAL', 'critical'); function report_security_hide_timearning() { global $PAGE; $PAGE->requires->js_init_code("Y.one('#timewarning').addClass('timewarninghidden')"); } function report_security_get_issue_list() { return array( 'report_security_check_unsecuredataroot', 'report_security_check_displayerrors', 'report_security_check_noauth', 'report_security_check_embed', 'report_security_check_mediafilterswf', 'report_security_check_openprofiles', 'report_security_check_google', 'report_security_check_passwordpolicy', 'report_security_check_emailchangeconfirmation', 'report_security_check_cookiesecure', 'report_security_check_configrw', 'report_security_check_riskxss', 'report_security_check_riskadmin', 'report_security_check_riskbackup', 'report_security_check_defaultuserrole', 'report_security_check_guestrole', 'report_security_check_frontpagerole', ); } function report_security_doc_link($issue, $name) { global $CFG, $OUTPUT; if (empty($CFG->docroot)) { return $name; } return $OUTPUT->doc_link('report/security/'.$issue, $name); } ///============================================= /// Issue checks ///============================================= /** * Verifies unsupported noauth setting * @param bool $detailed * @return object result */ function report_security_check_noauth($detailed=false) { global $CFG; $result = new stdClass(); $result->issue = 'report_security_check_noauth'; $result->name = get_string('check_noauth_name', 'report_security'); $result->info = null; $result->details = null; $result->status = null; $result->link = null; $result->link = "wwwroot/$CFG->admin/settings.php?section=manageauths\">".get_string('authsettings', 'admin').''; if (is_enabled_auth('none')) { $result->status = REPORT_SECURITY_CRITICAL; $result->info = get_string('check_noauth_error', 'report_security'); } else { $result->status = REPORT_SECURITY_OK; $result->info = get_string('check_noauth_ok', 'report_security'); } if ($detailed) { $result->details = get_string('check_noauth_details', 'report_security'); } return $result; } /** * Verifies if password policy set * @param bool $detailed * @return object result */ function report_security_check_passwordpolicy($detailed=false) { global $CFG; $result = new stdClass(); $result->issue = 'report_security_check_passwordpolicy'; $result->name = get_string('check_passwordpolicy_name', 'report_security'); $result->info = null; $result->details = null; $result->status = null; $result->link = "wwwroot/$CFG->admin/settings.php?section=sitepolicies\">".get_string('sitepolicies', 'admin').''; if (empty($CFG->passwordpolicy)) { $result->status = REPORT_SECURITY_WARNING; $result->info = get_string('check_passwordpolicy_error', 'report_security'); } else { $result->status = REPORT_SECURITY_OK; $result->info = get_string('check_passwordpolicy_ok', 'report_security'); } if ($detailed) { $result->details = get_string('check_passwordpolicy_details', 'report_security'); } return $result; } /** * Verifies sloppy embedding - this should have been removed long ago!! * @param bool $detailed * @return object result */ function report_security_check_embed($detailed=false) { global $CFG; $result = new stdClass(); $result->issue = 'report_security_check_embed'; $result->name = get_string('check_embed_name', 'report_security'); $result->info = null; $result->details = null; $result->status = null; $result->link = "wwwroot/$CFG->admin/settings.php?section=sitepolicies\">".get_string('sitepolicies', 'admin').''; if (!empty($CFG->allowobjectembed)) { $result->status = REPORT_SECURITY_CRITICAL; $result->info = get_string('check_embed_error', 'report_security'); } else { $result->status = REPORT_SECURITY_OK; $result->info = get_string('check_embed_ok', 'report_security'); } if ($detailed) { $result->details = get_string('check_embed_details', 'report_security'); } return $result; } /** * Verifies sloppy swf embedding - this should have been removed long ago!! * @param bool $detailed * @return object result */ function report_security_check_mediafilterswf($detailed=false) { global $CFG; $result = new stdClass(); $result->issue = 'report_security_check_mediafilterswf'; $result->name = get_string('check_mediafilterswf_name', 'report_security'); $result->info = null; $result->details = null; $result->status = null; $result->link = "wwwroot/$CFG->admin/settings.php?section=filtersettingfiltermediaplugin\">".get_string('filtersettings', 'admin').''; $activefilters = filter_get_globally_enabled(); if (array_search('mediaplugin', $activefilters) !== false and !empty($CFG->filter_mediaplugin_enable_swf)) { $result->status = REPORT_SECURITY_CRITICAL; $result->info = get_string('check_mediafilterswf_error', 'report_security'); } else { $result->status = REPORT_SECURITY_OK; $result->info = get_string('check_mediafilterswf_ok', 'report_security'); } if ($detailed) { $result->details = get_string('check_mediafilterswf_details', 'report_security'); } return $result; } /** * Verifies fatal misconfiguration of dataroot * @param bool $detailed * @return object result */ function report_security_check_unsecuredataroot($detailed=false) { global $CFG; $result = new stdClass(); $result->issue = 'report_security_check_unsecuredataroot'; $result->name = get_string('check_unsecuredataroot_name', 'report_security'); $result->info = null; $result->details = null; $result->status = null; $result->link = null; $insecuredataroot = is_dataroot_insecure(true); if ($insecuredataroot == INSECURE_DATAROOT_WARNING) { $result->status = REPORT_SECURITY_SERIOUS; $result->info = get_string('check_unsecuredataroot_warning', 'report_security', $CFG->dataroot); } else if ($insecuredataroot == INSECURE_DATAROOT_ERROR) { $result->status = REPORT_SECURITY_CRITICAL; $result->info = get_string('check_unsecuredataroot_error', 'report_security', $CFG->dataroot); } else { $result->status = REPORT_SECURITY_OK; $result->info = get_string('check_unsecuredataroot_ok', 'report_security'); } if ($detailed) { $result->details = get_string('check_unsecuredataroot_details', 'report_security'); } return $result; } /** * Verifies displaying of errors - problem for lib files and 3rd party code * because we can not disable debugging in these scripts (they do not include config.php) * @param bool $detailed * @return object result */ function report_security_check_displayerrors($detailed=false) { $result = new stdClass(); $result->issue = 'report_security_check_displayerrors'; $result->name = get_string('check_displayerrors_name', 'report_security'); $result->info = null; $result->details = null; $result->status = null; $result->link = null; if (defined('WARN_DISPLAY_ERRORS_ENABLED')) { $result->status = REPORT_SECURITY_WARNING; $result->info = get_string('check_displayerrors_error', 'report_security'); } else { $result->status = REPORT_SECURITY_OK; $result->info = get_string('check_displayerrors_ok', 'report_security'); } if ($detailed) { $result->details = get_string('check_displayerrors_details', 'report_security'); } return $result; } /** * Verifies open profiles - originally open by default, not anymore because spammer abused it a lot * @param bool $detailed * @return object result */ function report_security_check_openprofiles($detailed=false) { global $CFG; $result = new stdClass(); $result->issue = 'report_security_check_openprofiles'; $result->name = get_string('check_openprofiles_name', 'report_security'); $result->info = null; $result->details = null; $result->status = null; $result->link = "wwwroot/$CFG->admin/settings.php?section=sitepolicies\">".get_string('sitepolicies', 'admin').''; if (empty($CFG->forcelogin) and empty($CFG->forceloginforprofiles)) { $result->status = REPORT_SECURITY_WARNING; $result->info = get_string('check_openprofiles_error', 'report_security'); } else { $result->status = REPORT_SECURITY_OK; $result->info = get_string('check_openprofiles_ok', 'report_security'); } if ($detailed) { $result->details = get_string('check_openprofiles_details', 'report_security'); } return $result; } /** * Verifies google access not combined with disabled guest access * because attackers might gain guest access by modifying browser signature. * @param bool $detailed * @return object result */ function report_security_check_google($detailed=false) { global $CFG; $result = new stdClass(); $result->issue = 'report_security_check_google'; $result->name = get_string('check_google_name', 'report_security'); $result->info = null; $result->details = null; $result->status = null; $result->link = "wwwroot/$CFG->admin/settings.php?section=sitepolicies\">".get_string('sitepolicies', 'admin').''; if (empty($CFG->opentogoogle)) { $result->status = REPORT_SECURITY_OK; $result->info = get_string('check_google_ok', 'report_security'); } else if (!empty($CFG->guestloginbutton)) { $result->status = REPORT_SECURITY_INFO; $result->info = get_string('check_google_info', 'report_security'); } else { $result->status = REPORT_SECURITY_SERIOUS; $result->info = get_string('check_google_error', 'report_security'); } if ($detailed) { $result->details = get_string('check_google_details', 'report_security'); } return $result; } /** * Verifies email confirmation - spammers were changing mails very often * @param bool $detailed * @return object result */ function report_security_check_emailchangeconfirmation($detailed=false) { global $CFG; $result = new stdClass(); $result->issue = 'report_security_check_emailchangeconfirmation'; $result->name = get_string('check_emailchangeconfirmation_name', 'report_security'); $result->info = null; $result->details = null; $result->status = null; $result->link = "wwwroot/$CFG->admin/settings.php?section=sitepolicies\">".get_string('sitepolicies', 'admin').''; if (empty($CFG->emailchangeconfirmation)) { if (empty($CFG->allowemailaddresses)) { $result->status = REPORT_SECURITY_WARNING; $result->info = get_string('check_emailchangeconfirmation_error', 'report_security'); } else { $result->status = REPORT_SECURITY_INFO; $result->info = get_string('check_emailchangeconfirmation_info', 'report_security'); } } else { $result->status = REPORT_SECURITY_OK; $result->info = get_string('check_emailchangeconfirmation_ok', 'report_security'); } if ($detailed) { $result->details = get_string('check_emailchangeconfirmation_details', 'report_security'); } return $result; } /** * Verifies if https enabled only secure cookies allowed, * this prevents redirections and sending of cookies to unsecure port. * @param bool $detailed * @return object result */ function report_security_check_cookiesecure($detailed=false) { global $CFG; if (!is_https()) { return null; } $result = new stdClass(); $result->issue = 'report_security_check_cookiesecure'; $result->name = get_string('check_cookiesecure_name', 'report_security'); $result->info = null; $result->details = null; $result->status = null; $result->link = "wwwroot/$CFG->admin/settings.php?section=httpsecurity\">".get_string('httpsecurity', 'admin').''; if (empty($CFG->cookiesecure)) { $result->status = REPORT_SECURITY_SERIOUS; $result->info = get_string('check_cookiesecure_error', 'report_security'); } else { $result->status = REPORT_SECURITY_OK; $result->info = get_string('check_cookiesecure_ok', 'report_security'); } if ($detailed) { $result->details = get_string('check_cookiesecure_details', 'report_security'); } return $result; } /** * Verifies config.php is not writable anymore after installation, * config files were changed on several outdated server. * @param bool $detailed * @return object result */ function report_security_check_configrw($detailed=false) { global $CFG; $result = new stdClass(); $result->issue = 'report_security_check_configrw'; $result->name = get_string('check_configrw_name', 'report_security'); $result->info = null; $result->details = null; $result->status = null; $result->link = null; if (is_writable($CFG->dirroot.'/config.php')) { $result->status = REPORT_SECURITY_WARNING; $result->info = get_string('check_configrw_warning', 'report_security'); } else { $result->status = REPORT_SECURITY_OK; $result->info = get_string('check_configrw_ok', 'report_security'); } if ($detailed) { $result->details = get_string('check_configrw_details', 'report_security'); } return $result; } /** * Lists all users with XSS risk, it would be great to combine this with risk trusts in user table, * unfortunately nobody implemented user trust UI yet :-( * @param bool $detailed * @return object result */ function report_security_check_riskxss($detailed=false) { global $DB; $result = new stdClass(); $result->issue = 'report_security_check_riskxss'; $result->name = get_string('check_riskxss_name', 'report_security'); $result->info = null; $result->details = null; $result->status = REPORT_SECURITY_WARNING; $result->link = null; $params = array('capallow'=>CAP_ALLOW); $sqlfrom = "FROM (SELECT rcx.* FROM {role_capabilities} rcx JOIN {capabilities} cap ON (cap.name = rcx.capability AND ".$DB->sql_bitand('cap.riskbitmask', RISK_XSS)." <> 0) WHERE rcx.permission = :capallow) rc, {context} c, {context} sc, {role_assignments} ra, {user} u WHERE c.id = rc.contextid AND (sc.path = c.path OR sc.path LIKE ".$DB->sql_concat('c.path', "'/%'")." OR c.path LIKE ".$DB->sql_concat('sc.path', "'/%'").") AND u.id = ra.userid AND u.deleted = 0 AND ra.contextid = sc.id AND ra.roleid = rc.roleid"; $count = $DB->count_records_sql("SELECT COUNT(DISTINCT u.id) $sqlfrom", $params); $result->info = get_string('check_riskxss_warning', 'report_security', $count); if ($detailed) { $userfields = user_picture::fields('u'); $users = $DB->get_records_sql("SELECT DISTINCT $userfields $sqlfrom", $params); foreach ($users as $uid=>$user) { $users[$uid] = fullname($user); } $users = implode(', ', $users); $result->details = get_string('check_riskxss_details', 'report_security', $users); } return $result; } /** * Verifies sanity of default user role. * @param bool $detailed * @return object result */ function report_security_check_defaultuserrole($detailed=false) { global $DB, $CFG; $result = new stdClass(); $result->issue = 'report_security_check_defaultuserrole'; $result->name = get_string('check_defaultuserrole_name', 'report_security'); $result->info = null; $result->details = null; $result->status = null; $result->link = "wwwroot/$CFG->admin/settings.php?section=userpolicies\">".get_string('userpolicies', 'admin').''; if (!$default_role = $DB->get_record('role', array('id'=>$CFG->defaultuserroleid))) { $result->status = REPORT_SECURITY_WARNING; $result->info = get_string('check_defaultuserrole_notset', 'report_security'); $result->details = $result->info; return $result; } // risky caps - usually very dangerous $params = array('capallow'=>CAP_ALLOW, 'roleid'=>$default_role->id); $sql = "SELECT COUNT(DISTINCT rc.contextid) FROM {role_capabilities} rc JOIN {capabilities} cap ON cap.name = rc.capability WHERE ".$DB->sql_bitand('cap.riskbitmask', (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))." <> 0 AND rc.permission = :capallow AND rc.roleid = :roleid"; $riskycount = $DB->count_records_sql($sql, $params); // it may have either none or 'user' archetype - nothing else, or else it would break during upgrades badly if ($default_role->archetype === '' or $default_role->archetype === 'user') { $legacyok = true; } else { $legacyok = false; } if ($riskycount or !$legacyok) { $result->status = REPORT_SECURITY_CRITICAL; $result->info = get_string('check_defaultuserrole_error', 'report_security', role_get_name($default_role)); } else { $result->status = REPORT_SECURITY_OK; $result->info = get_string('check_defaultuserrole_ok', 'report_security'); } if ($detailed) { $result->details = get_string('check_defaultuserrole_details', 'report_security'); } return $result; } /** * Verifies sanity of guest role * @param bool $detailed * @return object result */ function report_security_check_guestrole($detailed=false) { global $DB, $CFG; $result = new stdClass(); $result->issue = 'report_security_check_guestrole'; $result->name = get_string('check_guestrole_name', 'report_security'); $result->info = null; $result->details = null; $result->status = null; $result->link = "wwwroot/$CFG->admin/settings.php?section=userpolicies\">".get_string('userpolicies', 'admin').''; if (!$guest_role = $DB->get_record('role', array('id'=>$CFG->guestroleid))) { $result->status = REPORT_SECURITY_WARNING; $result->info = get_string('check_guestrole_notset', 'report_security'); $result->details = $result->info; return $result; } // risky caps - usually very dangerous $params = array('capallow'=>CAP_ALLOW, 'roleid'=>$guest_role->id); $sql = "SELECT COUNT(DISTINCT rc.contextid) FROM {role_capabilities} rc JOIN {capabilities} cap ON cap.name = rc.capability WHERE ".$DB->sql_bitand('cap.riskbitmask', (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))." <> 0 AND rc.permission = :capallow AND rc.roleid = :roleid"; $riskycount = $DB->count_records_sql($sql, $params); // it may have either no or 'guest' archetype - nothing else, or else it would break during upgrades badly if ($guest_role->archetype === '' or $guest_role->archetype === 'guest') { $legacyok = true; } else { $legacyok = false; } if ($riskycount or !$legacyok) { $result->status = REPORT_SECURITY_CRITICAL; $result->info = get_string('check_guestrole_error', 'report_security', format_string($guest_role->name)); } else { $result->status = REPORT_SECURITY_OK; $result->info = get_string('check_guestrole_ok', 'report_security'); } if ($detailed) { $result->details = get_string('check_guestrole_details', 'report_security'); } return $result; } /** * Verifies sanity of frontpage role * @param bool $detailed * @return object result */ function report_security_check_frontpagerole($detailed=false) { global $DB, $CFG; $result = new stdClass(); $result->issue = 'report_security_check_frontpagerole'; $result->name = get_string('check_frontpagerole_name', 'report_security'); $result->info = null; $result->details = null; $result->status = null; $result->link = "wwwroot/$CFG->admin/settings.php?section=frontpagesettings\">".get_string('frontpagesettings','admin').''; if (!$frontpage_role = $DB->get_record('role', array('id'=>$CFG->defaultfrontpageroleid))) { $result->status = REPORT_SECURITY_INFO; $result->info = get_string('check_frontpagerole_notset', 'report_security'); $result->details = get_string('check_frontpagerole_details', 'report_security'); return $result; } // risky caps - usually very dangerous $params = array('capallow'=>CAP_ALLOW, 'roleid'=>$frontpage_role->id); $sql = "SELECT COUNT(DISTINCT rc.contextid) FROM {role_capabilities} rc JOIN {capabilities} cap ON cap.name = rc.capability WHERE ".$DB->sql_bitand('cap.riskbitmask', (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))." <> 0 AND rc.permission = :capallow AND rc.roleid = :roleid"; $riskycount = $DB->count_records_sql($sql, $params); // there is no legacy role type for frontpage yet - anyway we can not allow teachers or admins there! if ($frontpage_role->archetype === 'teacher' or $frontpage_role->archetype === 'editingteacher' or $frontpage_role->archetype === 'coursecreator' or $frontpage_role->archetype === 'manager') { $legacyok = false; } else { $legacyok = true; } if ($riskycount or !$legacyok) { $result->status = REPORT_SECURITY_CRITICAL; $result->info = get_string('check_frontpagerole_error', 'report_security', format_string($frontpage_role->name)); } else { $result->status = REPORT_SECURITY_OK; $result->info = get_string('check_frontpagerole_ok', 'report_security'); } if ($detailed) { $result->details = get_string('check_frontpagerole_details', 'report_security'); } return $result; } /** * Lists all admins. * @param bool $detailed * @return object result */ function report_security_check_riskadmin($detailed=false) { global $DB, $CFG; $result = new stdClass(); $result->issue = 'report_security_check_riskadmin'; $result->name = get_string('check_riskadmin_name', 'report_security'); $result->info = null; $result->details = null; $result->status = null; $result->link = null; $userfields = user_picture::fields('u'); $sql = "SELECT $userfields FROM {user} u WHERE u.id IN ($CFG->siteadmins)"; $admins = $DB->get_records_sql($sql); $admincount = count($admins); if ($detailed) { foreach ($admins as $uid=>$user) { $url = "$CFG->wwwroot/user/view.php?id=$user->id"; $admins[$uid] = '