Projet

Général

Profil

Paste
Télécharger (30,9 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / security_review / security_review.inc @ bb746689

1
<?php
2

    
3
/**
4
 * @file
5
 * Stand-alone security checks and review system.
6
 */
7

    
8
// Define the version of this file in case it's used outside of its module.
9
define('SECURITY_REVIEW_VERSION', '7.x-1.1');
10

    
11
/**
12
 * Public function for running Security Review checklist and returning results.
13
 *
14
 * @param array $checklist Array of checks to run, indexed by module namespace.
15
 * @param boolean $log Whether to log check processing using security_review_log.
16
 * @param boolean $help Whether to load the help file and include in results.
17
 * @return array Results from running checklist, indexed by module namespace.
18
 */
19
function security_review_run($checklist = NULL, $log = FALSE, $help = FALSE) {
20
  if (!$checklist && module_exists('security_review')) {
21
    $checklist = module_invoke_all('security_checks');
22
  }
23
  $results = _security_review_run($checklist, $log);
24

    
25
  // Fill out the descriptions of the results if $help is requested.
26
  if ($help && module_exists('security_review') && module_load_include('inc', 'security_review', 'security_review.help')) {
27
    foreach ($results as $module => $checks) {
28
      foreach ($checks as $check_name => $check) {
29
        $function = $check['callback'] . '_help';
30
        // We should have loaded all necessary include files.
31
        if (function_exists($function)) {
32
          $element = call_user_func($function, $check);
33
          // @todo run through theme?
34
          $results[$module][$check_name]['help'] = $element;
35
        }
36
      }
37
    }
38
  }
39

    
40
  return $results;
41
}
42

    
43
/**
44
 * Private function the review and returns the full results.
45
 */
46
function _security_review_run($checklist, $log = FALSE) {
47
  $results = array();
48
  foreach ($checklist as $module => $checks) {
49
    foreach ($checks as $check_name => $arguments) {
50
      $check_result = _security_review_run_check($module, $check_name, $arguments, $log);
51
      if (!empty($check_result)) {
52
        $results[$module][$check_name] = $check_result;
53
      }
54
    }
55
  }
56
  return $results;
57
}
58

    
59
/**
60
 * Run a single Security Review check.
61
 */
62
function _security_review_run_check($module, $check_name, $check, $log, $store = FALSE) {
63
  $last_check = array();
64
  if ($store) {
65
    // Get the results of the last check.
66
    $last_check = security_review_get_last_check($module, $check_name);
67
  }
68
  $check_result = array();
69
  $return = array('result' => NULL);
70
  if (isset($check['file'])) {
71
    // Handle Security Review defining checks for other modules.
72
    if (isset($check['module'])) {
73
      $module = $check['module'];
74
    }
75
    module_load_include('inc', $module, $check['file']);
76
  }
77
  $function = $check['callback'];
78
  if (function_exists($function)) {
79
    $return = call_user_func($function, $last_check);
80
  }
81
  $check_result = array_merge($check, $return);
82
  $check_result['lastrun'] = REQUEST_TIME;
83

    
84
  if ($log && !is_null($return['result'])) { // Do not log if result is NULL.
85
    $variables = array('!name' => $check_result['title']);
86
    if ($check_result['result']) {
87
      _security_review_log($module, $check_name, '!name check passed', $variables, WATCHDOG_INFO);
88
    }
89
    else {
90
      _security_review_log($module, $check_name, '!name check failed', $variables, WATCHDOG_ERROR);
91
    }
92
  }
93
  return $check_result;
94
}
95

    
96
/**
97
 * Core Security Review's checks.
98
 *
99
 * @see security_review_get_checklist().
100
 */
101
function _security_review_security_checks() {
102
  $checks = array();
103
  $checks['file_perms'] = array(
104
    'title' => t('File system permissions'),
105
    'callback' => 'security_review_check_file_perms',
106
    'success' => t('Drupal installation files and directories (except required) are not writable by the server.'),
107
    'failure' => t('Some files and directories in your install are writable by the server.'),
108
  );
109
  $checks['input_formats'] = array(
110
    'title' => t('Text formats'),
111
    'callback' => 'security_review_check_input_formats',
112
    'success' => t('Untrusted users are not allowed to input dangerous HTML tags.'),
113
    'failure' => t('Untrusted users are allowed to input dangerous HTML tags.'),
114
  );
115
  $checks['field'] = array(
116
    'title' => t('Content'),
117
    'callback' => 'security_review_check_field',
118
    'success' => t('Dangerous tags were not found in any submitted content (fields).'),
119
    'failure' => t('Dangerous tags were found in submitted content (fields).'),
120
  );
121
  $checks['error_reporting'] = array(
122
    'title' => t('Error reporting'),
123
    'callback' => 'security_review_check_error_reporting',
124
    'success' => t('Error reporting set to log only.'),
125
    'failure' => t('Errors are written to the screen.'),
126
  );
127
  $checks['private_files'] = array(
128
    'title' => t('Private files'),
129
    'callback' => 'security_review_check_private_files',
130
    'success' => t('Private files directory is outside the web server root.'),
131
    'failure' => t('Private files is enabled but the specified directory is not secure outside the web server root.'),
132
  );
133
  // Checks dependent on dblog.
134
  if (module_exists('dblog')) {
135
    $checks['query_errors'] = array(
136
      'title' => t('Database errors'),
137
      'callback' => 'security_review_check_query_errors',
138
      'success' => t('Few query errors from the same IP.'),
139
      'failure' => t('Query errors from the same IP. These may be a SQL injection attack or an attempt at information disclosure.'),
140
    );
141

    
142
    $checks['failed_logins'] = array(
143
      'title' => t('Failed logins'),
144
      'callback' => 'security_review_check_failed_logins',
145
      'success' => t('Few failed login attempts from the same IP.'),
146
      'failure' => t('Failed login attempts from the same IP. These may be a brute-force attack to gain access to your site.'),
147
    );
148
  }
149
  $checks['upload_extensions'] = array(
150
    'title' => t('Allowed upload extensions'),
151
    'callback' => 'security_review_check_upload_extensions',
152
    'success' => t('Only safe extensions are allowed for uploaded files and images.'),
153
    'failure' => t('Unsafe file extensions are allowed in uploads.'),
154
  );
155
  $checks['admin_permissions'] = array(
156
    'title' => t('Drupal permissions'),
157
    'callback' => 'security_review_check_admin_permissions',
158
    'success' => t('Untrusted roles do not have administrative or trusted Drupal permissions.'),
159
    'failure' => t('Untrusted roles have been granted administrative or trusted Drupal permissions.'),
160
  );
161
  /*$checks['name_passwords'] = array(
162
    'title' => t('Username as password'),
163
    'callback' => 'security_review_check_name_passwords',
164
    'success' => t('Trusted accounts do not have their password set to their username.'),
165
    'failure' => t('Some trusted accounts have set their password the same as their username.'),
166
  );*/
167
  // Check dependent on PHP filter being enabled.
168
  if (module_exists('php')) {
169
    $checks['untrusted_php'] = array(
170
      'title' => t('PHP access'),
171
      'callback' => 'security_review_check_php_filter',
172
      'success' => t('Untrusted users do not have access to use the PHP input format.'),
173
      'failure' => t('Untrusted users have access to use the PHP input format.'),
174
    );
175
  }
176
  $checks['executable_php'] = array(
177
    'title' => t('Executable PHP'),
178
    'callback' => 'security_review_check_executable_php',
179
    'success' => t('PHP files in the Drupal files directory cannot be executed.'),
180
    'failure' => t('PHP files in the Drupal files directory can be executed.'),
181
  );
182
  $checks['base_url_set'] = array(
183
    'title' => t('Drupal base URL'),
184
    'callback' => 'security_review_check_base_url',
185
    'success' => t('Base URL is set in settings.php.'),
186
    'failure' => t('Base URL is not set in settings.php.'),
187
  );
188
  $checks['temporary_files'] = array(
189
    'title' => t('Temporary files'),
190
    'callback' => 'security_review_check_temporary_files',
191
    'success' => t('No sensitive temporary files were found.'),
192
    'failure' => t('Sensitive temporary files were found on your files system.'),
193
  );
194

    
195
  return array('security_review' => $checks);
196
}
197

    
198
/**
199
 * Checks for security_review_get_checklist() when Views is enabled.
200
 */
201
function views_security_checks() {
202
  $checks = array();
203
  $checks['access'] = array(
204
    'title' => t('Views access'),
205
    'callback' => 'security_review_check_views_access',
206
    'success' => t('Views are access controlled.'),
207
    'failure' => t('There are Views that do not provide any access checks.'),
208
    'module' => 'security_review',
209
    // Specify this file because the callback is here.
210
    'file' => 'security_review',
211
  );
212
  return array('views' => $checks);
213
}
214

    
215
/**
216
 * Check that files aren't writeable by the server.
217
 */
218
function security_review_check_file_perms() {
219
  $result = TRUE;
220
  // Extract ending folder for file directory path.
221
  $file_path = './' . rtrim(variable_get('file_public_path', conf_path() . '/files'), '/');
222
  // Set files to ignore.
223
  $ignore = array('..', 'CVS', '.git', '.svn', '.bzr', realpath($file_path));
224
  // Add temporary files directory if it's set.
225
  $temp_path = variable_get('file_temporary_path', '');
226
  if (!empty($temp_path)) {
227
    $ignore[] = realpath('./' . rtrim($temp_path, '/'));
228
  }
229
  // Add private files directory if it's set.
230
  $private_files = variable_get('file_private_path', '');
231
  if (!empty($private_files)) {
232
    // Remove leading slash if set.
233
    if (strrpos($private_files, '/') !== FALSE) {
234
      $private_files = substr($private_files, strrpos($private_files, '/') + 1);
235
    }
236
    $ignore[] = $private_files;
237
  }
238
  drupal_alter('security_review_file_ignore', $ignore);
239
  $parsed = array(realpath('.'));
240
  $files = _security_review_check_file_perms_scan('.', $parsed, $ignore);
241

    
242
  // Try creating or appending files.
243
  // Assume it doesn't work.
244
  $create_status = $append_status = FALSE;
245

    
246
  $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.");
247

    
248
  $directory = drupal_get_path('module', 'security_review');
249
  // Write a file with the timestamp
250
  $file = './' . $directory . '/file_write_test.' . date('Ymdhis');
251
  if ($file_create = @fopen($file, 'w')) {
252
    $create_status = fwrite($file_create, date('Ymdhis') . ' - ' . $append_message . "\n");
253
    fclose($file_create);
254
  }
255
  // Try to append to our IGNOREME file.
256
  $file = './'. $directory . '/IGNOREME.txt';
257
  if ($file_append = @fopen($file, 'a')) {
258
    $append_status = fwrite($file_append, date('Ymdhis') . ' - ' . $append_message . "\n");
259
    fclose($file_append);
260
  }
261

    
262
  if (count($files) || $create_status || $append_status) {
263
    $result = FALSE;
264
  }
265
  return array('result' => $result, 'value' => $files);
266
}
267

    
268
function _security_review_check_file_perms_scan($directory, &$parsed, $ignore) {
269
  $items = array();
270
  if ($handle = opendir($directory)) {
271
    while (($file = readdir($handle)) !== FALSE) {
272
      // Don't check hidden files or ones we said to ignore.
273
      $path = $directory . "/" . $file;
274
      if ($file[0] != "." && !in_array($file, $ignore) && !in_array(realpath($path), $ignore)) {
275
        if (is_dir($path) && !in_array(realpath($path), $parsed)) {
276
          $parsed[] = realpath($path);
277
          $items = array_merge($items, _security_review_check_file_perms_scan($path, $parsed, $ignore));
278
          if (is_writable($path)) {
279
            $items[] = preg_replace("/\/\//si", "/", $path);
280
          }
281
        }
282
        elseif (is_writable($path)) {
283
          $items[] = preg_replace("/\/\//si", "/", $path);
284
        }
285
      }
286

    
287
    }
288
    closedir($handle);
289
  }
290
  return $items;
291
}
292

    
293
/**
294
 * Check for formats that either do not have HTML filter that can be used by
295
 * untrusted users, or if they do check if unsafe tags are allowed.
296
 */
297
function security_review_check_input_formats() {
298
  $result = TRUE;
299
  $formats = filter_formats();
300
  $check_result_value = array();
301
  // Check formats that are accessible by untrusted users.
302
  $untrusted_roles = security_review_untrusted_roles();
303
  $untrusted_roles = array_keys($untrusted_roles);
304
  foreach ($formats as $id => $format) {
305
    $format_roles = filter_get_roles_by_format($format);
306
    $intersect = array_intersect(array_keys($format_roles), $untrusted_roles);
307
    if (!empty($intersect)) {
308
      // Untrusted users can use this format.
309
      $filters = filter_list_format($format->format);
310
      // Check format for enabled HTML filter.
311
      if (in_array('filter_html', array_keys($filters)) && $filters['filter_html']->status) {
312
        $filter = $filters['filter_html'];
313
        // Check for unsafe tags in allowed tags.
314
        $allowed_tags = $filter->settings['allowed_html'];
315
        $unsafe_tags = security_review_unsafe_tags();
316
        drupal_alter('security_review_unsafe_tags', $unsafe_tags);
317
        foreach ($unsafe_tags as $tag) {
318
          if (strpos($allowed_tags, '<' . $tag . '>') !== FALSE) {
319
            // Found an unsafe tag
320
            $check_result_value['tags'][$id] = $tag;
321
          }
322
        }
323
      }
324
      elseif (!in_array('filter_html_escape', array_keys($filters)) || !$filters['filter_html_escape']->status) {
325
        // Format is usable by untrusted users but does not contain the HTML Filter or the HTML escape.
326
        $check_result_value['formats'][$id] = $format;
327
      }
328
    }
329
  }
330

    
331
  if (!empty($check_result_value)) {
332
    $result = FALSE;
333
  }
334
  return array('result' => $result, 'value' => $check_result_value);
335
}
336

    
337
function security_review_check_php_filter() {
338
  $result = TRUE;
339
  $formats = filter_formats();
340
  $check_result_value = array();
341
  // Check formats that are accessible by untrusted users.
342
  $untrusted_roles = security_review_untrusted_roles();
343
  $untrusted_roles = array_keys($untrusted_roles);
344
  foreach ($formats as $id => $format) {
345
    $format_roles = filter_get_roles_by_format($format);
346
    $intersect = array_intersect(array_keys($format_roles), $untrusted_roles);
347
    if (!empty($intersect)) {
348
      // Untrusted users can use this format.
349
      $filters = filter_list_format($format->format);
350
      // Check format for enabled PHP filter.
351
      if (in_array('php_code', array_keys($filters)) && $filters['php_code']->status) {
352
        $result = FALSE;
353
        $check_result_value['formats'][$id] = $format;
354
      }
355
    }
356
  }
357

    
358
  return array('result' => $result, 'value' => $check_result_value);
359
}
360

    
361
function security_review_check_error_reporting() {
362
  $error_level = variable_get('error_level', NULL);
363
  if (is_null($error_level) || intval($error_level) >= 1) {
364
    // When the variable isn't set, or its set to 1 or 2 errors are printed to the screen.
365
    $result = FALSE;
366
  }
367
  else {
368
    $result = TRUE;
369
  }
370
  return array('result' => $result, 'value' => $error_level);
371
}
372

    
373
/**
374
 * If private files is enabled check that the directory is not under the web root.
375
 *
376
 * There is ample room for the user to get around this check. @TODO get more sophisticated?
377
 */
378
function security_review_check_private_files() {
379
  $file_directory_path = variable_get('file_private_path', '');
380
  if (empty($file_directory_path)) {
381
    $result = NULL; // Ignore this check.
382
  }
383
  elseif (strpos(realpath($file_directory_path), DRUPAL_ROOT) === 0) {
384
    // Path begins at root.
385
    $result = FALSE;
386
  }
387
  else {
388
    $result = TRUE;
389
  }
390
  return array('result' => $result, 'value' => $file_directory_path);
391
}
392

    
393
function security_review_check_query_errors($last_check = NULL) {
394
  $timestamp = NULL;
395
  $check_result_value = array();
396
  $query = db_select('watchdog', 'w')->fields('w', array('message', 'hostname'))
397
    ->condition('type', 'php')
398
    ->condition('severity', WATCHDOG_ERROR);
399
  if (isset($last_check['lastrun'])) {
400
    $query->condition('timestamp', $last_check['lastrun'], '>=');
401
  }
402
  $result = $query->execute();
403
  foreach ($result as $row) {
404
    if (strpos($row->message, 'SELECT') !== FALSE) {
405
      $entries[$row->hostname][] = $row;
406
    }
407
  }
408
  $result = TRUE;
409
  if (!empty($entries)) {
410
    foreach ($entries as $ip => $records) {
411
      if (count($records) > 10) {
412
        $check_result_value[] = $ip;
413
      }
414
    }
415
  }
416
  if (!empty($check_result_value)) {
417
    $result = FALSE;
418
  }
419
  else {
420
    // Rather than worrying the user about the idea of query errors we skip reporting a pass.
421
    $result = NULL;
422
  }
423
  return array('result' => $result, 'value' => $check_result_value);
424
}
425

    
426
function security_review_check_failed_logins($last_check = NULL) {
427
  $result = TRUE;
428
  $timestamp = NULL;
429
  $check_result_value = array();
430
  $query = db_select('watchdog', 'w')->fields('w', array('message', 'hostname'))
431
    ->condition('type', 'php')
432
    ->condition('severity', WATCHDOG_NOTICE);
433
  if (isset($last_check['lastrun'])) {
434
    $query->condition('timestamp', $last_check['lastrun'], '>=');
435
  }
436
  $result = $query->execute();
437
  foreach ($result as $row) {
438
    if (strpos($row->message, 'Login attempt failed') !== FALSE) {
439
      $entries[$row->hostname][] = $row;
440
    }
441
  }
442
  if (!empty($entries)) {
443
    foreach ($entries as $ip => $records) {
444
      if (count($records) > 10) {
445
        $check_result_value[] = $ip;
446
      }
447
    }
448
  }
449
  if (!empty($check_result_value)) {
450
    $result = FALSE;
451
  }
452
  else {
453
    // Rather than worrying the user about the idea of failed logins we skip reporting a pass.
454
    $result = NULL;
455
  }
456
  return array('result' => $result, 'value' => $check_result_value);
457
}
458

    
459
/**
460
 * Look for admin permissions granted to untrusted roles.
461
 */
462
function security_review_check_admin_permissions() {
463
  $result = TRUE;
464
  $check_result_value = array();
465
  $untrusted_roles = security_review_untrusted_roles();
466
  // Collect permissions marked as for trusted users only.
467
  $all_permissions = module_invoke_all('permission');
468
  $all_keys = array_keys($all_permissions);
469
  // Get permissions for untrusted roles.
470
  $untrusted_permissions = user_role_permissions($untrusted_roles);
471
  foreach ($untrusted_permissions as $rid => $permissions) {
472
    $intersect = array_intersect($all_keys, array_keys($permissions));
473
    foreach ($intersect as $permission) {
474
      if (isset($all_permissions[$permission]['restrict access'])) {
475
        $check_result_value[$rid][] = $permission;
476
      }
477
    }
478
  }
479

    
480
  if (!empty($check_result_value)) {
481
    $result = FALSE;
482
  }
483
  return array('result' => $result, 'value' => $check_result_value);
484
}
485

    
486
function security_review_check_field($last_check = NULL) {
487
  $check_result = TRUE;
488
  $check_result_value = $tables = $found = array();
489
  $timestamp = NULL;
490
  $instances = field_info_instances();
491
  // Loop through instances checking for fields of type text.
492
  foreach ($instances as $entity_type => $type_bundles) {
493
    foreach ($type_bundles as $bundle => $bundle_instances) {
494
      foreach ($bundle_instances as $field_name => $instance) {
495
        $field = field_info_field($field_name);
496
        // Check into text fields that are stored in SQL.
497
        if ($field['module'] == 'text' && $field['storage']['module'] == 'field_sql_storage') {
498
          // Build array of tables and columns to search.
499
          $current_table = key($field['storage']['details']['sql'][FIELD_LOAD_CURRENT]);
500
          $revision_table = key($field['storage']['details']['sql'][FIELD_LOAD_REVISION]);
501
          if (!array_key_exists($current_table, $tables)) {
502
            $tables[$current_table] = array(
503
              'column' => $field['storage']['details']['sql'][FIELD_LOAD_CURRENT][$current_table]['value'],
504
              'name' => $field['field_name'],
505
            );
506
          }
507
          if (!array_key_exists($revision_table, $tables)) {
508
            $tables[$revision_table] = array(
509
              'column' => $field['storage']['details']['sql'][FIELD_LOAD_REVISION][$revision_table]['value'],
510
              'name' => $field['field_name'],
511
            );
512
          }
513
        }
514
      }
515
    }
516
  }
517
  if (empty($tables)) {
518
    return array('result' => $check_result, 'value' => $check_result_value);
519
  }
520
  // Search for PHP or Javascript tags in text columns.
521
  foreach ($tables as $table => $info) {
522
    $sql = "SELECT DISTINCT entity_id, entity_type FROM {" . $table . "} WHERE " . $info['column'] . " LIKE :text";
523
    // Handle changed? @todo
524
    foreach (array('Javascript' => '%<script%', 'PHP' => '%<?php%') as $vuln_type => $comparison) {
525
      $results = db_query($sql, array(':text' => $comparison)); // @pager query?
526
      foreach ($results as $result) {
527
        $check_result = FALSE;
528
        if (!isset($check_result_value[$result->entity_type]) || !array_key_exists($result->entity_id, $check_result_value[$result->entity_type])) {
529
          $check_result_value[$result->entity_type][$result->entity_id] = array(
530
            'type' => $vuln_type,
531
            'field' => $info['name'],
532
          );
533
        }
534
      }
535
    }
536
  }
537

    
538
  return array('result' => $check_result, 'value' => $check_result_value);
539
}
540

    
541
function security_review_check_upload_extensions($last_check = NULL) {
542
  $check_result = TRUE;
543
  $check_result_value = array();
544
  $instances = field_info_instances();
545
  $unsafe_extensions = security_review_unsafe_extensions();
546
  // Loop through instances checking for fields of file.
547
  foreach ($instances as $entity_type => $type_bundles) {
548
    foreach ($type_bundles as $bundle => $bundle_instances) {
549
      foreach ($bundle_instances as $field_name => $instance) {
550
        $field = field_info_field($field_name);
551
        if ($field['module'] == 'image' || $field['module'] == 'file') {
552
          // Check instance file_extensions.
553
          foreach ($unsafe_extensions as $unsafe_extension) {
554
            if (strpos($instance['settings']['file_extensions'], $unsafe_extension) !== FALSE) {
555
              // Found an unsafe extension.
556
              $check_result_value[$instance['field_name']][$instance['bundle']] = $unsafe_extension;
557
              $check_result = FALSE;
558
            }
559
          }
560
        }
561
      }
562
    }
563
  }
564

    
565
  return array('result' => $check_result, 'value' => $check_result_value);
566
}
567

    
568
function security_review_check_name_passwords($last_check = NULL) {
569
  $result = TRUE;
570
  $check_result_value = array();
571
  $timestamp = NULL;
572

    
573
  // Check whether trusted roles have weak passwords.
574
  $trusted_roles = security_review_trusted_roles();
575
  if (!empty($trusted_roles)) {
576
    $trusted_roles = array_keys($trusted_roles);
577
    $check_result_value = _security_review_weak_passwords($trusted_roles);
578
  }
579
  if (!empty($check_result_value)) {
580
    $result = FALSE;
581
  }
582

    
583
  return array('result' => $result, 'value' => $check_result_value);
584
}
585

    
586
function _security_review_weak_passwords($trusted_roles) {
587
  $weak_users = array();
588

    
589
  // Select users with a trusted role whose password is their username.
590
  // @todo need to generate passwords in PHP to get salt.
591
  $sql = "SELECT u.uid, u.name, COUNT(rid) AS count FROM {users} u LEFT JOIN
592
    {users_roles} ur ON u.uid = ur.uid AND ur.rid in (:rids)
593
    WHERE pass = md5(name) GROUP BY uid";
594
  $results = db_query($sql, array(':rids' => $trusted_roles)); // @todo pager_query?
595
  foreach ($results as $row) {
596
    $record[] = $row;
597
    if ($row->count > 0) {
598
      $weak_users[$row->uid] = $row->name;
599
    }
600
  }
601

    
602
  // Explicitly check uid 1 in case they have no roles.
603
  $weak_uid1 = db_fetch_object(db_query("SELECT u.uid, u.name, 1 AS count FROM {users} u WHERE pass = md5(name) AND uid = 1"));
604
  if (!empty($weak_uid1->count)) {
605
    $weak_users[$weak_uid1->uid] = $weak_uid1->name;
606
  }
607

    
608
  return $weak_users;
609
}
610

    
611
/**
612
 * Check if PHP files written to the files directory can be executed.
613
 */
614
function security_review_check_executable_php($last_check = NULL) {
615
  global $base_url;
616
  $result = TRUE;
617
  $check_result_value = array();
618

    
619
  $message = 'Security review test ' . date('Ymdhis');
620
  $content = "<?php\necho '" . $message . "';";
621
  $directory = variable_get('file_public_path', 'sites/default/files');
622
  $file = '/security_review_test.php';
623
  if ($file_create = @fopen('./' . $directory . $file, 'w')) {
624
    $create_status = fwrite($file_create, $content);
625
    fclose($file_create);
626
  }
627
  $response = drupal_http_request($base_url . '/' . $directory . $file);
628
  if ($response->code == 200 && $response->data === $message) {
629
    $result = FALSE;
630
    $check_result_value[] = 'executable_php';
631
  }
632
  if (file_exists('./' . $directory . $file)) {
633
    @unlink('./' . $directory . $file);
634
  }
635
  // Check for presence of the .htaccess file and if the contents are correct.
636
  if (!file_exists($directory . '/.htaccess')) {
637
    $result = FALSE;
638
    $check_result_value[] = 'missing_htaccess';
639
  }
640
  elseif (!function_exists('file_htaccess_lines')) {
641
    $result = FALSE;
642
    $check_result_value[] = 'outdated_core';
643
  }
644
  else {
645
    $contents = file_get_contents($directory . '/.htaccess');
646
    // Text from includes/file.inc.
647
    $expected = file_htaccess_lines(FALSE);
648
    if (trim($contents) !== trim($expected)) {
649
      $result = FALSE;
650
      $check_result_value[] = 'incorrect_htaccess';
651
    }
652
    if (is_writable($directory . '/.htaccess')) {
653
      // Don't modify $result.
654
      $check_result_value[] = 'writable_htaccess';
655
    }
656
  }
657

    
658
  return array('result' => $result, 'value' => $check_result_value);
659
}
660

    
661
/**
662
 * Check if $base_url is set in settings.php.
663
 */
664
function security_review_check_base_url($last_check = NULL) {
665
  // Support different methods to check for $base_url.
666
  $method = variable_get('security_review_base_url_method', 'token');
667
  $result = NULL;
668
  if ($method === 'token') {
669
    if (file_exists(DRUPAL_ROOT . '/' . conf_path() . '/settings.php')) {
670
      $content = file_get_contents(DRUPAL_ROOT . '/' . conf_path() . '/settings.php');
671
      $tokens = token_get_all($content);
672
    }
673
    $result = FALSE;
674
    foreach ($tokens as $token) {
675
      if (is_array($token) && $token[0] === T_VARIABLE && $token[1] == '$base_url') {
676
        $result = TRUE;
677
        break;
678
      }
679
    }
680
  }
681
  elseif ($method === 'include') {
682
    if (file_exists(DRUPAL_ROOT . '/' . conf_path() . '/settings.php')) {
683
      include DRUPAL_ROOT . '/' . conf_path() . '/settings.php';
684
    }
685
    if (isset($base_url)) {
686
      $result = TRUE;
687
    }
688
    else {
689
      $result = FALSE;
690
    }
691
  }
692
  return array('result' => $result, 'value' => '');
693
}
694

    
695
/**
696
 * Check for sensitive temporary files like settings.php~.
697
 */
698
function security_review_check_temporary_files($last_check = NULL) {
699
  $result = TRUE;
700
  $check_result_value = array();
701
  $files = array();
702
  $dir = scandir(DRUPAL_ROOT . '/' . conf_path() . '/');
703
  foreach ($dir as $file) {
704
    // Set full path to only files.
705
    if (!is_dir($file)) {
706
      $files[] = DRUPAL_ROOT . '/' . conf_path() . '/' . $file;
707
    }
708
  }
709
  drupal_alter('security_review_temporary_files', $files);
710
  foreach ($files as $path) {
711
    $matches = array();
712
    if (file_exists($path) && preg_match('/.*(~|\.sw[op]|\.bak|\.orig|\.save)$/', $path, $matches) !== FALSE && !empty($matches)) {
713
      $result = FALSE;
714
      $check_result_value[] = $path;
715
    }
716
  }
717
  return array('result' => $result, 'value' => $check_result_value);
718
}
719

    
720
/**
721
 * Get core Security Review checks and checks from any hook_security_checks().
722
 *
723
 * @return array
724
 *   Array of checks indexed by module name. e.g.:
725
 *   'security_review' => array(
726
 *     'check_name' => array(...)
727
 *   )
728
 */
729
function security_review_get_checklist() {
730
  $checks = _security_review_security_checks();
731
  $checks = array_merge($checks, module_invoke_all('security_checks'));
732
  return $checks;
733
}
734

    
735
function security_review_check_views_access($last_check = NULL) {
736
  $result = TRUE;
737
  $check_result_value = array();
738
  $timestamp = NULL;
739
  // Load and loop through every view, checking the access type in displays.
740
  $views = views_get_all_views();
741
  foreach ($views as $view) {
742
    if ($view->disabled !== TRUE) {
743
      // Access is set in display options of a display.
744
      foreach ($view->display as $display_name => $display) {
745
        if (isset($display->display_options['access']) && $display->display_options['access']['type'] == 'none') {
746
          $check_result_value[$view->name][] = $display_name;
747
        }
748
      }
749
    }
750
  }
751
  if (!empty($check_result_value)) {
752
    $result = FALSE;
753
  }
754
  return array('result' => $result, 'value' => $check_result_value);
755
}
756

    
757
/**
758
 * Helper function defines HTML tags that are considered unsafe.
759
 *
760
 * Based on wysiwyg_filter_get_elements_blacklist(),
761
 * https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet, and
762
 * html5sec.org.
763
 */
764
function security_review_unsafe_tags() {
765
  return array(
766
    'applet',
767
    'area',
768
    'audio',
769
    'base',
770
    'basefont',
771
    'body',
772
    'button',
773
    'comment',
774
    'embed',
775
    'eval',
776
    'form',
777
    'frame',
778
    'frameset',
779
    'head',
780
    'html',
781
    'iframe',
782
    'image',
783
    'img',
784
    'input',
785
    'isindex',
786
    'label',
787
    'link',
788
    'map',
789
    'math',
790
    'meta',
791
    'noframes',
792
    'noscript',
793
    'object',
794
    'optgroup',
795
    'option',
796
    'param',
797
    'script',
798
    'select',
799
    'style',
800
    'svg',
801
    'table',
802
    'td',
803
    'textarea',
804
    'title',
805
    'video',
806
    'vmlframe',
807
  );
808
}
809

    
810
/**
811
 * Helper function defines file extensions considered unsafe.
812
 */
813
function security_review_unsafe_extensions() {
814
  return array(
815
    'swf',
816
    'exe',
817
    'html',
818
    'htm',
819
    'php',
820
    'phtml',
821
    'py',
822
    'js',
823
    'vb',
824
    'vbe',
825
    'vbs',
826
  );
827
}
828

    
829
/**
830
 * Helper function defines the default untrusted Drupal roles.
831
 */
832
function _security_review_default_untrusted_roles() {
833
  $roles = array(DRUPAL_ANONYMOUS_RID => t('anonymous user'));
834
  $user_register = variable_get('user_register', 1);
835
  // If visitors are allowed to create accounts they are considered untrusted.
836
  if ($user_register != USER_REGISTER_ADMINISTRATORS_ONLY) {
837
    $roles[DRUPAL_AUTHENTICATED_RID] = t('authenticated user');
838
  }
839
  return $roles;
840
}
841

    
842
/**
843
 * Helper function for user-defined or default unstrusted Drupal roles.
844
 *
845
 * @return An associative array with the role id as the key and the role name as value.
846
 */
847
function security_review_untrusted_roles() {
848
  $defaults = _security_review_default_untrusted_roles();
849
  $roles = variable_get('security_review_untrusted_roles', $defaults);
850
  // array_filter() to remove roles with 0 (meaning they are trusted) @todo
851
  return array_filter($roles);
852
}
853

    
854
/**
855
 * Helper function collects the permissions untrusted roles have.
856
 */
857
function security_review_untrusted_permissions() {
858
  static $permissions;
859
  if (empty($permissions)) {
860
    $permissions = array();
861
    // Collect list of untrusted roles' permissions.
862
    $untrusted_roles = security_review_untrusted_roles();
863
    foreach ($untrusted_roles as $rid) {
864
      $perms = array();
865
      $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))
866
        ->fetchArray();
867
      if ($results !== FALSE) {
868
        $perms = explode(',', str_replace(', ', ',', $results['permission']));
869
        $permissions[$rid] = $perms;
870
      }
871
    }
872
  }
873
  return $permissions;
874
}
875

    
876
/**
877
 * Helper function for assumed trusted roles.
878
 */
879
function security_review_trusted_roles() {
880
  $trusted_roles = array();
881
  $untrusted_roles = security_review_untrusted_roles();
882
  $results = db_query('SELECT rid, name FROM {role} WHERE rid NOT IN (:rids)', array(':rids' => $untrusted_roles));
883
  foreach ($results as $role) {
884
    $trusted_roles[$role->rid] = $role->name;
885
  }
886
  return array_filter($trusted_roles);
887
}
888

    
889
/**
890
 * Check if role has been granted a permission.
891
 */
892
function security_review_role_permission($rid, $permission) {
893
  $return = FALSE;
894
  $result = db_select('role_permission', 'p')->fields('p', array('permission'))->condition('rid', $rid)->execute()->fetchField();
895
  if ($result['permission'] && strpos($result['permission'], $permission) !== FALSE) {
896
    $return = TRUE;
897
  }
898
  return $return;
899
}
900

    
901
function _security_review_log($module, $check_name, $message, $variables, $type) {
902
  module_invoke_all('security_review_log', $module, $check_name, $message, $variables, $type);
903
}