View source
<?php
define('SECURITY_REVIEW_VERSION', '7.x-1.1');
function security_review_run($checklist = NULL, $log = FALSE, $help = FALSE) {
if (!$checklist && module_exists('security_review')) {
$checklist = module_invoke_all('security_checks');
}
$results = _security_review_run($checklist, $log);
if ($help && module_exists('security_review') && module_load_include('inc', 'security_review', 'security_review.help')) {
foreach ($results as $module => $checks) {
foreach ($checks as $check_name => $check) {
$function = $check['callback'] . '_help';
if (function_exists($function)) {
$element = call_user_func($function, $check);
$results[$module][$check_name]['help'] = $element;
}
}
}
}
return $results;
}
function _security_review_run($checklist, $log = FALSE) {
$results = array();
foreach ($checklist as $module => $checks) {
foreach ($checks as $check_name => $arguments) {
$check_result = _security_review_run_check($module, $check_name, $arguments, $log);
if (!empty($check_result)) {
$results[$module][$check_name] = $check_result;
}
}
}
return $results;
}
function _security_review_run_check($module, $check_name, $check, $log, $store = FALSE) {
$variables = array(
'@title' => $check['title'],
'@name' => $check_name,
);
_security_review_log($module, $check_name, 'Beginning the @title (@name) security review check.', $variables, WATCHDOG_INFO);
$last_check = array();
if ($store) {
$last_check = security_review_get_last_check($module, $check_name);
}
$check_result = array();
$return = array(
'result' => NULL,
);
if (isset($check['file'])) {
if (isset($check['module'])) {
$module = $check['module'];
}
module_load_include('inc', $module, $check['file']);
}
$function = $check['callback'];
if (function_exists($function)) {
$return = call_user_func($function, $last_check);
}
$check_result = array_merge($check, $return);
$check_result['lastrun'] = REQUEST_TIME;
if ($log && !is_null($return['result'])) {
if ($check_result['result']) {
_security_review_log($module, $check_name, '@title check passed', $variables, WATCHDOG_INFO);
}
else {
_security_review_log($module, $check_name, '@title check failed', $variables, WATCHDOG_ERROR);
}
}
return $check_result;
}
function _security_review_security_checks() {
$checks = array();
$checks['file_perms'] = array(
'title' => t('File system permissions'),
'callback' => 'security_review_check_file_perms',
'success' => t('Drupal installation files and directories (except required) are not writable by the server.'),
'failure' => t('Some files and directories in your install are writable by the server.'),
);
$checks['input_formats'] = array(
'title' => t('Text formats'),
'callback' => 'security_review_check_input_formats',
'success' => t('Untrusted users are not allowed to input dangerous HTML tags.'),
'failure' => t('Untrusted users are allowed to input dangerous HTML tags.'),
);
$checks['field'] = array(
'title' => t('Content'),
'callback' => 'security_review_check_field',
'success' => t('Dangerous tags were not found in any submitted content (fields).'),
'failure' => t('Dangerous tags were found in submitted content (fields).'),
);
$checks['error_reporting'] = array(
'title' => t('Error reporting'),
'callback' => 'security_review_check_error_reporting',
'success' => t('Error reporting set to log only.'),
'failure' => t('Errors are written to the screen.'),
);
$checks['private_files'] = array(
'title' => t('Private files'),
'callback' => 'security_review_check_private_files',
'success' => t('Private files directory is outside the web server root.'),
'failure' => t('Private files is enabled but the specified directory is not secure outside the web server root.'),
);
if (module_exists('dblog')) {
$checks['query_errors'] = array(
'title' => t('Database errors'),
'callback' => 'security_review_check_query_errors',
'success' => t('Few query errors from the same IP.'),
'failure' => t('Query errors from the same IP. These may be a SQL injection attack or an attempt at information disclosure.'),
);
$checks['failed_logins'] = array(
'title' => t('Failed logins'),
'callback' => 'security_review_check_failed_logins',
'success' => t('Few failed login attempts from the same IP.'),
'failure' => t('Failed login attempts from the same IP. These may be a brute-force attack to gain access to your site.'),
);
}
$checks['upload_extensions'] = array(
'title' => t('Allowed upload extensions'),
'callback' => 'security_review_check_upload_extensions',
'success' => t('Only safe extensions are allowed for uploaded files and images.'),
'failure' => t('Unsafe file extensions are allowed in uploads.'),
);
$checks['admin_permissions'] = array(
'title' => t('Drupal permissions'),
'callback' => 'security_review_check_admin_permissions',
'success' => t('Untrusted roles do not have administrative or trusted Drupal permissions.'),
'failure' => t('Untrusted roles have been granted administrative or trusted Drupal permissions.'),
);
$checks['name_passwords'] = array(
'title' => t('Username as password'),
'callback' => 'security_review_check_name_passwords',
'success' => t('Trusted accounts do not have their password set to their username.'),
'failure' => t('Some trusted accounts have set their password the same as their username.'),
);
if (module_exists('php')) {
$checks['untrusted_php'] = array(
'title' => t('PHP access'),
'callback' => 'security_review_check_php_filter',
'success' => t('Untrusted users do not have access to use the PHP input format.'),
'failure' => t('Untrusted users have access to use the PHP input format.'),
);
}
$checks['executable_php'] = array(
'title' => t('Executable PHP'),
'callback' => 'security_review_check_executable_php',
'success' => t('PHP files in the Drupal files directory cannot be executed.'),
'failure' => t('PHP files in the Drupal files directory can be executed.'),
);
$checks['base_url_set'] = array(
'title' => t('Drupal base URL'),
'callback' => 'security_review_check_base_url',
'success' => t('Base URL is set in settings.php.'),
'failure' => t('Base URL is not set in settings.php.'),
);
$checks['temporary_files'] = array(
'title' => t('Temporary files'),
'callback' => 'security_review_check_temporary_files',
'success' => t('No sensitive temporary files were found.'),
'failure' => t('Sensitive temporary files were found on your files system.'),
);
return array(
'security_review' => $checks,
);
}
function views_security_checks() {
$checks = array();
$checks['access'] = array(
'title' => t('Views access'),
'callback' => 'security_review_check_views_access',
'success' => t('Views are access controlled.'),
'failure' => t('There are Views that do not provide any access checks.'),
'module' => 'security_review',
'file' => 'security_review',
);
return array(
'views' => $checks,
);
}
function security_review_check_file_perms() {
$result = TRUE;
$file_path = './' . rtrim(variable_get('file_public_path', conf_path() . '/files'), '/');
$ignore = array(
'..',
'CVS',
'.git',
'.svn',
'.bzr',
realpath($file_path),
);
$temp_path = variable_get('file_temporary_path', '');
if (!empty($temp_path)) {
$ignore[] = realpath('./' . rtrim($temp_path, '/'));
}
$private_files = variable_get('file_private_path', '');
if (!empty($private_files)) {
if (strrpos($private_files, '/') !== FALSE) {
$private_files = substr($private_files, strrpos($private_files, '/') + 1);
}
$ignore[] = $private_files;
}
drupal_alter('security_review_file_ignore', $ignore);
$parsed = array(
realpath('.'),
);
$files = _security_review_check_file_perms_scan('.', $parsed, $ignore);
$create_status = $append_status = FALSE;
$append_message = t("Your web server should not be able to write to your modules directory. This is a security vulnerable. Consult the Security Review file permissions check help for mitigation steps.");
$directory = drupal_get_path('module', 'security_review');
$file = './' . $directory . '/file_write_test.' . date('Ymdhis');
if ($file_create = @fopen($file, 'w')) {
$create_status = fwrite($file_create, date('Ymdhis') . ' - ' . $append_message . "\n");
fclose($file_create);
}
$file = './' . $directory . '/IGNOREME.txt';
if ($file_append = @fopen($file, 'a')) {
$append_status = fwrite($file_append, date('Ymdhis') . ' - ' . $append_message . "\n");
fclose($file_append);
}
if (count($files) || $create_status || $append_status) {
$result = FALSE;
}
return array(
'result' => $result,
'value' => $files,
);
}
function _security_review_check_file_perms_scan($directory, &$parsed, $ignore) {
$items = array();
if ($handle = opendir($directory)) {
while (($file = readdir($handle)) !== FALSE) {
$path = $directory . "/" . $file;
if ($file[0] != "." && !in_array($file, $ignore) && !in_array(realpath($path), $ignore)) {
if (is_dir($path) && !in_array(realpath($path), $parsed)) {
$parsed[] = realpath($path);
$items = array_merge($items, _security_review_check_file_perms_scan($path, $parsed, $ignore));
if (is_writable($path)) {
$items[] = preg_replace("/\\/\\//si", "/", $path);
}
}
elseif (is_writable($path)) {
$items[] = preg_replace("/\\/\\//si", "/", $path);
}
}
}
closedir($handle);
}
return $items;
}
function security_review_check_input_formats() {
$result = TRUE;
$formats = filter_formats();
$check_result_value = array();
$untrusted_roles = security_review_untrusted_roles();
$untrusted_roles = array_keys($untrusted_roles);
foreach ($formats as $id => $format) {
$format_roles = filter_get_roles_by_format($format);
$intersect = array_intersect(array_keys($format_roles), $untrusted_roles);
if (!empty($intersect)) {
$filters = filter_list_format($format->format);
if (in_array('filter_html', array_keys($filters)) && $filters['filter_html']->status) {
$filter = $filters['filter_html'];
$allowed_tags = $filter->settings['allowed_html'];
$unsafe_tags = security_review_unsafe_tags();
drupal_alter('security_review_unsafe_tags', $unsafe_tags);
foreach ($unsafe_tags as $tag) {
if (strpos($allowed_tags, '<' . $tag . '>') !== FALSE) {
$check_result_value['tags'][$id] = $tag;
}
}
}
elseif (!in_array('filter_html_escape', array_keys($filters)) || !$filters['filter_html_escape']->status) {
$check_result_value['formats'][$id] = $format;
}
}
}
if (!empty($check_result_value)) {
$result = FALSE;
}
return array(
'result' => $result,
'value' => $check_result_value,
);
}
function security_review_check_php_filter() {
$result = TRUE;
$formats = filter_formats();
$check_result_value = array();
$untrusted_roles = security_review_untrusted_roles();
$untrusted_roles = array_keys($untrusted_roles);
foreach ($formats as $id => $format) {
$format_roles = filter_get_roles_by_format($format);
$intersect = array_intersect(array_keys($format_roles), $untrusted_roles);
if (!empty($intersect)) {
$filters = filter_list_format($format->format);
if (in_array('php_code', array_keys($filters)) && $filters['php_code']->status) {
$result = FALSE;
$check_result_value['formats'][$id] = $format;
}
}
}
return array(
'result' => $result,
'value' => $check_result_value,
);
}
function security_review_check_error_reporting() {
$error_level = variable_get('error_level', NULL);
if (is_null($error_level) || intval($error_level) >= 1) {
$result = FALSE;
}
else {
$result = TRUE;
}
return array(
'result' => $result,
'value' => $error_level,
);
}
function security_review_check_private_files() {
$file_directory_path = variable_get('file_private_path', '');
if (empty($file_directory_path)) {
$result = NULL;
}
elseif (strpos(realpath($file_directory_path), DRUPAL_ROOT) === 0) {
$result = FALSE;
}
else {
$result = TRUE;
}
return array(
'result' => $result,
'value' => $file_directory_path,
);
}
function security_review_check_query_errors($last_check = NULL) {
$timestamp = NULL;
$check_result_value = array();
$query = db_select('watchdog', 'w')
->fields('w', array(
'message',
'hostname',
))
->condition('type', 'php')
->condition('severity', WATCHDOG_ERROR);
if (isset($last_check['lastrun'])) {
$query
->condition('timestamp', $last_check['lastrun'], '>=');
}
$result = $query
->execute();
foreach ($result as $row) {
if (strpos($row->message, 'SELECT') !== FALSE) {
$entries[$row->hostname][] = $row;
}
}
$result = TRUE;
if (!empty($entries)) {
foreach ($entries as $ip => $records) {
if (count($records) > 10) {
$check_result_value[] = $ip;
}
}
}
if (!empty($check_result_value)) {
$result = FALSE;
}
else {
$result = NULL;
}
return array(
'result' => $result,
'value' => $check_result_value,
);
}
function security_review_check_failed_logins($last_check = NULL) {
$result = TRUE;
$timestamp = NULL;
$check_result_value = array();
$query = db_select('watchdog', 'w')
->fields('w', array(
'message',
'hostname',
))
->condition('type', 'php')
->condition('severity', WATCHDOG_NOTICE);
if (isset($last_check['lastrun'])) {
$query
->condition('timestamp', $last_check['lastrun'], '>=');
}
$result = $query
->execute();
foreach ($result as $row) {
if (strpos($row->message, 'Login attempt failed') !== FALSE) {
$entries[$row->hostname][] = $row;
}
}
if (!empty($entries)) {
foreach ($entries as $ip => $records) {
if (count($records) > 10) {
$check_result_value[] = $ip;
}
}
}
if (!empty($check_result_value)) {
$result = FALSE;
}
else {
$result = NULL;
}
return array(
'result' => $result,
'value' => $check_result_value,
);
}
function security_review_check_admin_permissions() {
$result = TRUE;
$check_result_value = array();
$untrusted_roles = security_review_untrusted_roles();
$all_permissions = module_invoke_all('permission');
$all_keys = array_keys($all_permissions);
$untrusted_permissions = user_role_permissions($untrusted_roles);
foreach ($untrusted_permissions as $rid => $permissions) {
$intersect = array_intersect($all_keys, array_keys($permissions));
foreach ($intersect as $permission) {
if (!empty($all_permissions[$permission]['restrict access'])) {
$check_result_value[$rid][] = $permission;
}
}
}
if (!empty($check_result_value)) {
$result = FALSE;
}
return array(
'result' => $result,
'value' => $check_result_value,
);
}
function security_review_check_field($last_check = NULL) {
$check_result = TRUE;
$check_result_value = $tables = $found = array();
$timestamp = NULL;
$instances = field_info_instances();
foreach ($instances as $entity_type => $type_bundles) {
foreach ($type_bundles as $bundle => $bundle_instances) {
foreach ($bundle_instances as $field_name => $instance) {
$field = field_info_field($field_name);
if ($field['module'] == 'text' && $field['storage']['module'] == 'field_sql_storage') {
$current_table = key($field['storage']['details']['sql'][FIELD_LOAD_CURRENT]);
$revision_table = key($field['storage']['details']['sql'][FIELD_LOAD_REVISION]);
if (!array_key_exists($current_table, $tables)) {
$tables[$current_table] = array(
'column' => $field['storage']['details']['sql'][FIELD_LOAD_CURRENT][$current_table]['value'],
'name' => $field['field_name'],
);
}
if (!array_key_exists($revision_table, $tables)) {
$tables[$revision_table] = array(
'column' => $field['storage']['details']['sql'][FIELD_LOAD_REVISION][$revision_table]['value'],
'name' => $field['field_name'],
);
}
}
}
}
}
if (empty($tables)) {
return array(
'result' => $check_result,
'value' => $check_result_value,
);
}
$known_risky_fields = explode(',', variable_get('security_review_known_risky_fields', ''));
foreach ($tables as $table => $info) {
$sql = "SELECT DISTINCT entity_id, entity_type, " . $info['column'] . " AS field_text FROM {" . $table . "} WHERE " . $info['column'] . " LIKE :text";
foreach (array(
'Javascript' => '%<script%',
'PHP' => '%<?php%',
) as $vuln_type => $comparison) {
$results = db_query($sql, array(
':text' => $comparison,
));
foreach ($results as $result) {
if (!isset($check_result_value[$result->entity_type]) || !array_key_exists($result->entity_id, $check_result_value[$result->entity_type])) {
$hash = hash('sha256', implode((array) $result));
if (!in_array($hash, $known_risky_fields)) {
$check_result = FALSE;
$check_result_value[$result->entity_type][$result->entity_id] = array(
'type' => $vuln_type,
'field' => $info['name'],
'hash' => $hash,
);
}
}
}
}
}
return array(
'result' => $check_result,
'value' => $check_result_value,
);
}
function security_review_check_upload_extensions($last_check = NULL) {
$check_result = TRUE;
$check_result_value = array();
$instances = field_info_instances();
$unsafe_extensions = security_review_unsafe_extensions();
foreach ($instances as $entity_type => $type_bundles) {
foreach ($type_bundles as $bundle => $bundle_instances) {
foreach ($bundle_instances as $field_name => $instance) {
$field = field_info_field($field_name);
if ($field['module'] == 'image' || $field['module'] == 'file') {
foreach ($unsafe_extensions as $unsafe_extension) {
if (strpos($instance['settings']['file_extensions'], $unsafe_extension) !== FALSE) {
$check_result_value[$instance['field_name']][$instance['bundle']] = $unsafe_extension;
$check_result = FALSE;
}
}
}
}
}
}
return array(
'result' => $check_result,
'value' => $check_result_value,
);
}
function security_review_check_name_passwords($last_check = NULL) {
$result = TRUE;
$check_result_value = array();
$timestamp = NULL;
$trusted_roles = security_review_trusted_roles();
if (!empty($trusted_roles)) {
$trusted_roles = array_keys($trusted_roles);
$check_result_value = _security_review_weak_passwords($trusted_roles);
}
if (!empty($check_result_value)) {
$result = FALSE;
}
return array(
'result' => $result,
'value' => $check_result_value,
);
}
function _security_review_weak_passwords($trusted_roles) {
require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc');
$weak_users = array();
$query = db_select('users', 'u');
$query
->leftJoin('users_roles', 'ur', 'u.uid = ur.uid AND ur.rid IN (:rids)', array(
':rids' => $trusted_roles,
));
$query
->fields('u', array(
'uid',
'name',
'pass',
));
$query
->addExpression('COUNT(rid)', 'count');
$query
->groupBy('u.uid');
$query
->groupBy('u.name');
$query
->groupBy('u.pass');
$results = $query
->execute()
->fetchAllAssoc('uid');
foreach ($results as $row) {
if ($row->count > 0 || $row->uid == 1) {
$account = (object) array(
'uid' => $row->uid,
'name' => $row->name,
'pass' => $row->pass,
);
if (user_check_password($row->name, $account)) {
$weak_users[$row->uid] = $row->name;
}
}
}
return $weak_users;
}
function security_review_check_executable_php($last_check = NULL) {
global $base_url;
$result = TRUE;
$check_result_value = array();
$message = 'Security review test ' . date('Ymdhis');
$content = "<?php\necho '" . $message . "';";
$directory = variable_get('file_public_path', 'sites/default/files');
$file = '/security_review_test.php';
if ($file_create = @fopen('./' . $directory . $file, 'w')) {
$create_status = fwrite($file_create, $content);
fclose($file_create);
}
$response = drupal_http_request($base_url . '/' . $directory . $file);
if ($response->code == 200 && $response->data === $message) {
$result = FALSE;
$check_result_value[] = 'executable_php';
}
if (file_exists('./' . $directory . $file)) {
@unlink('./' . $directory . $file);
}
if (!file_exists($directory . '/.htaccess')) {
$result = FALSE;
$check_result_value[] = 'missing_htaccess';
}
elseif (!function_exists('file_htaccess_lines')) {
$result = FALSE;
$check_result_value[] = 'outdated_core';
}
else {
$contents = file_get_contents($directory . '/.htaccess');
$expected = file_htaccess_lines(FALSE);
if (trim($contents) !== trim($expected)) {
$result = FALSE;
$check_result_value[] = 'incorrect_htaccess';
}
if (is_writable($directory . '/.htaccess')) {
$check_result_value[] = 'writable_htaccess';
}
}
return array(
'result' => $result,
'value' => $check_result_value,
);
}
function security_review_check_base_url($last_check = NULL) {
$method = variable_get('security_review_base_url_method', 'token');
$result = NULL;
if ($method === 'token') {
if (file_exists(DRUPAL_ROOT . '/' . conf_path() . '/settings.php')) {
$content = file_get_contents(DRUPAL_ROOT . '/' . conf_path() . '/settings.php');
$tokens = token_get_all($content);
}
$result = FALSE;
foreach ($tokens as $token) {
if (is_array($token) && $token[0] === T_VARIABLE && $token[1] == '$base_url') {
$result = TRUE;
break;
}
}
}
elseif ($method === 'include') {
if (file_exists(DRUPAL_ROOT . '/' . conf_path() . '/settings.php')) {
include DRUPAL_ROOT . '/' . conf_path() . '/settings.php';
}
if (isset($base_url)) {
$result = TRUE;
}
else {
$result = FALSE;
}
}
return array(
'result' => $result,
'value' => '',
);
}
function security_review_check_temporary_files($last_check = NULL) {
$result = TRUE;
$check_result_value = array();
$files = array();
$dir = scandir(DRUPAL_ROOT . '/' . conf_path() . '/');
foreach ($dir as $file) {
if (!is_dir($file)) {
$files[] = DRUPAL_ROOT . '/' . conf_path() . '/' . $file;
}
}
drupal_alter('security_review_temporary_files', $files);
foreach ($files as $path) {
$matches = array();
if (file_exists($path) && preg_match('/.*(~|\\.sw[op]|\\.bak|\\.orig|\\.save)$/', $path, $matches) !== FALSE && !empty($matches)) {
$result = FALSE;
$check_result_value[] = $path;
}
}
return array(
'result' => $result,
'value' => $check_result_value,
);
}
function security_review_get_checklist() {
$checks = _security_review_security_checks();
$checks = array_merge($checks, module_invoke_all('security_checks'));
return $checks;
}
function security_review_check_views_access($last_check = NULL) {
$result = TRUE;
$check_result_value = array();
$timestamp = NULL;
$views = views_get_all_views();
foreach ($views as $view) {
if ($view->disabled !== TRUE) {
foreach ($view->display as $display_name => $display) {
if (isset($display->display_options['access']) && $display->display_options['access']['type'] == 'none') {
$check_result_value[$view->name][] = $display_name;
}
}
}
}
if (!empty($check_result_value)) {
$result = FALSE;
}
return array(
'result' => $result,
'value' => $check_result_value,
);
}
function security_review_unsafe_tags() {
return array(
'applet',
'area',
'audio',
'base',
'basefont',
'body',
'button',
'comment',
'embed',
'eval',
'form',
'frame',
'frameset',
'head',
'html',
'iframe',
'image',
'img',
'input',
'isindex',
'label',
'link',
'map',
'math',
'meta',
'noframes',
'noscript',
'object',
'optgroup',
'option',
'param',
'script',
'select',
'style',
'svg',
'table',
'td',
'textarea',
'title',
'video',
'vmlframe',
);
}
function security_review_unsafe_extensions() {
return array(
'swf',
'exe',
'html',
'htm',
'php',
'phtml',
'py',
'js',
'vb',
'vbe',
'vbs',
);
}
function _security_review_default_untrusted_roles() {
$roles = array(
DRUPAL_ANONYMOUS_RID => t('anonymous user'),
);
$user_register = variable_get('user_register', 1);
if ($user_register != USER_REGISTER_ADMINISTRATORS_ONLY) {
$roles[DRUPAL_AUTHENTICATED_RID] = t('authenticated user');
}
return $roles;
}
function security_review_untrusted_roles() {
$defaults = _security_review_default_untrusted_roles();
$roles = variable_get('security_review_untrusted_roles', $defaults);
return array_filter($roles);
}
function security_review_untrusted_permissions() {
static $permissions;
if (empty($permissions)) {
$permissions = array();
$untrusted_roles = security_review_untrusted_roles();
foreach ($untrusted_roles as $rid) {
$perms = array();
$results = db_query('SELECT r.rid, p.permission FROM {role} r LEFT JOIN {role_permission} p ON r.rid = p.rid WHERE r.rid = :rid', array(
':rid' => $rid,
))
->fetchArray();
if ($results !== FALSE) {
$perms = explode(',', str_replace(', ', ',', $results['permission']));
$permissions[$rid] = $perms;
}
}
}
return $permissions;
}
function security_review_trusted_roles() {
$trusted_roles = array();
$untrusted_roles = security_review_untrusted_roles();
$results = db_query('SELECT rid, name FROM {role} WHERE rid NOT IN (:rids)', array(
':rids' => $untrusted_roles,
));
foreach ($results as $role) {
$trusted_roles[$role->rid] = $role->name;
}
return array_filter($trusted_roles);
}
function security_review_role_permission($rid, $permission) {
$return = FALSE;
$result = db_select('role_permission', 'p')
->fields('p', array(
'permission',
))
->condition('rid', $rid)
->execute()
->fetchField();
if ($result['permission'] && strpos($result['permission'], $permission) !== FALSE) {
$return = TRUE;
}
return $return;
}
function _security_review_log($module, $check_name, $message, $variables, $type) {
module_invoke_all('security_review_log', $module, $check_name, $message, $variables, $type);
}