You are here

file_upload_security.module in File Upload Security 7

Same filename and directory in other branches
  1. 7.3 file_upload_security.module

Helper module to advise and resolve security issues in file uploads.

File

file_upload_security.module
View source
<?php

/**
 * @file
 * Helper module to advise and resolve security issues in file uploads.
 */
define('FILE_UPLOAD_SECURITY_WARN', 'warn');
define('FILE_UPLOAD_SECURITY_PROTECT', 'protect');
define('FILE_UPLOAD_SECURITY_PSA', 'https://www.drupal.org/psa-2016-003');

/**
 * Implements hook_menu().
 */
function file_upload_security_menu() {
  return array(
    'admin/config/media/file_upload_security' => array(
      'title' => 'File Upload Security settings',
      'description' => 'Configuration for the File Upload Security module.',
      'page callback' => 'drupal_get_form',
      'page arguments' => array(
        'file_upload_security_admin_form',
      ),
      'access arguments' => array(
        'administer file upload security',
      ),
      'file' => 'includes/file_upload_security.admin.inc',
    ),
  );
}

/**
 * Implements hook_permission().
 */
function file_upload_security_permission() {
  return array(
    'administer file upload security' => array(
      'title' => t('Administer File Upload Security'),
      'description' => t('Configure the File Upload Security settings.'),
    ),
  );
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function file_upload_security_form_field_ui_field_edit_form_alter(&$form, &$form_state) {
  if (array_key_exists('#field', $form)) {
    if (array_key_exists('type', $form['#field']) && in_array($form['#field']['type'], file_upload_security_field_types())) {
      if (array_key_exists('#instance', $form)) {
        if (array_key_exists('bundle', $form['#instance'])) {
          $bundle = $form['#instance']['bundle'] ? $form['#instance']['bundle'] : NULL;
          $type = $form['#instance']['entity_type'] ? $form['#instance']['entity_type'] : NULL;
          if ($bundle && $type && file_upload_security_is_affected($bundle, $type)) {
            if (array_key_exists('field', $form) && array_key_exists('settings', $form['field'])) {
              if (array_key_exists('uri_scheme', $form['field']['settings'])) {
                file_upload_security_amend_widget_settings($form['field']['settings']['uri_scheme'], $bundle);
              }
            }
          }
        }
      }
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function file_upload_security_form_field_ui_field_settings_form_alter(&$form, &$form_state) {
  if (array_key_exists('field', $form) && array_key_exists('type', $form['field'])) {
    if (array_key_exists('#value', $form['field']['type']) && in_array($form['field']['type']['#value'], file_upload_security_field_types())) {
      $bundle = array_key_exists('#bundle', $form) ? $form['#bundle'] : NULL;
      $type = array_key_exists('#entity_type', $form) ? $form['#entity_type'] : NULL;
      if (file_upload_security_is_affected($bundle, $type)) {
        if (array_key_exists('settings', $form['field'])) {
          if (array_key_exists('uri_scheme', $form['field']['settings'])) {
            file_upload_security_amend_widget_settings($form['field']['settings']['uri_scheme'], $bundle);
          }
        }
      }
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function file_upload_security_form_webform_component_edit_form_alter(&$form, &$form_state) {
  if (array_key_exists('type', $form) && array_key_exists('#value', $form['type'])) {
    if (in_array($form['type']['#value'], file_upload_security_field_types())) {
      if (array_key_exists('extra', $form) && array_key_exists('scheme', $form['extra'])) {
        file_upload_security_amend_widget_settings($form['extra']['scheme'], 'webform submission');
      }
    }
  }
}

/**
 * Implements hook_webform_component_presave().
 */
function file_upload_security_webform_component_presave(&$component) {
  if (array_key_exists('type', $component) && $component['type'] == 'file') {
    if (array_key_exists('extra', $component)) {
      $extra =& $component['extra'];
      if (array_key_exists('private', $extra) && $extra['private'] != TRUE) {
        if (array_key_exists('scheme', $extra)) {
          if (variable_get('file_upload_security_level', FILE_UPLOAD_SECURITY_WARN) == FILE_UPLOAD_SECURITY_PROTECT) {
            $extra['scheme'] = 'private';
            file_upload_security_set_message('webform submission');
          }
        }
      }
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function file_upload_security_form_form_builder_field_configure_alter(&$form, &$form_state) {
  if (array_key_exists('webform_file_scheme', $form)) {
    file_upload_security_amend_widget_settings($form['webform_file_scheme'], 'webform submission');
  }
}

/**
 * Helper to carry out the file_upload_security action based on settings.
 *
 * @param array $element
 *   The widget form settings element array.
 * @param string $bundle
 *   The bundle the field is being edited on.
 */
function file_upload_security_amend_widget_settings(&$element, $bundle) {
  $action = variable_get('file_upload_security_level', FILE_UPLOAD_SECURITY_WARN);
  if (array_key_exists('#options', $element)) {
    $options =& $element['#options'];
    $default =& $element['#default_value'];
    if ($action == FILE_UPLOAD_SECURITY_WARN && $default == 'public') {
      file_upload_security_set_message($bundle, 'warn');
    }
    elseif ($action == FILE_UPLOAD_SECURITY_PROTECT) {
      unset($options['public']);
      $default = 'private';
      file_upload_security_set_message($bundle);
    }
  }
}

/**
 * Helper to discover is a given entity bundle/combination is affected.
 *
 * @param string $bundle
 *   A valid drupal entity bundle.
 * @param string $entity_type
 *   A valid Drupal entity type.
 *
 * @return bool
 *   If entity/bundle combination requires securing. Defaults to FALSE.
 */
function file_upload_security_is_affected($bundle, $entity_type) {
  $is_affected = FALSE;
  $affected_files = file_upload_security_affected_types();
  if ($bundle && $entity_type) {
    if (array_key_exists($entity_type, $affected_files['entities'])) {
      if (in_array($bundle, $affected_files['entities'][$entity_type])) {
        $is_affected = TRUE;
      }
    }
  }
  return $is_affected;
}

/**
 * Helper to set a message about protecting the file structure.
 *
 * @param string $bundle
 *   The content type being protected.
 * @param string $type
 *   The message to set: protect or warn.
 */
function file_upload_security_set_message($bundle, $type = 'protect') {
  $psa_link = l(t('the Drupal Security Team public service announcement'), FILE_UPLOAD_SECURITY_PSA);
  if ($type == 'protect') {
    $message = t('The site administrator has restricted file uploads to prevent anonymous users uploading malicious files to public areas. See !url for further details.', array(
      '@bundle' => $bundle,
      '!url' => $psa_link,
    ));
  }
  else {
    $message = t('The @bundle content type can be created by anonymous users. Allowing those users to upload files into the public file storage can be a security risk. See !url for further details.', array(
      '@bundle' => $bundle,
      '!url' => $psa_link,
    ));
  }
  drupal_set_message($message, 'warning');
}

/**
 * Helper to move files in insecure locations to the private file system.
 */
function file_upload_security_fix_files() {
  if (variable_get('file_private_path', NULL)) {
    $files = array();
    file_upload_security_fix_fields($files);
    if (module_exists('webform')) {
      file_upload_security_fix_webforms($files);
    }
    if ($files) {
      file_upload_security_move_fixed_files($files);
    }
  }
  else {
    watchdog('file_upload_security', 'Attempted to fix insecure files without private file system being set.', array(), WATCHDOG_ERROR);
    drupal_set_message(t('Legacy files cannot be moved without a private file system being configured. Please set up a private file path at !url.', array(
      '!url' => l(t('the file system admin page'), '/admin/config/media/file-system'),
    )));
  }
}

/**
 * Avoid use of batches when fixing files via drush.
 */
function file_upload_security_fix_files_drush() {
  $files = array();
  file_upload_security_fix_fields($files);
  if (module_exists('webform')) {
    file_upload_security_fix_webforms($files);
  }
  if ($files) {
    foreach ($files as $fid) {
      $context = array();
      file_upload_security_fix_move_file_batch($fid, $context);
    }
  }
}

/**
 * Updates field settings to be secure.
 *
 * @param array $files
 *   Passed by reference to collect fids that may require moving on server.
 */
function file_upload_security_fix_fields(&$files = array()) {
  $affected_fields = file_upload_security_affected_types();
  if ($affected_fields['fields']) {
    foreach ($affected_fields['fields'] as $field => $bundles) {
      $info = field_info_field($field);
      if ($info && array_key_exists('settings', $info)) {
        if ($info['settings']['uri_scheme'] != 'private') {
          $info['settings']['uri_scheme'] = 'private';
          field_update_field($info);
          $updated_fields[$field] = $bundles;
        }
        $field_table = isset($info['storage']['details']['sql']['FIELD_LOAD_CURRENT']) ? key($info['storage']['details']['sql']['FIELD_LOAD_CURRENT']) : NULL;
        $field_column = isset($info['storage']['details']['sql']['FIELD_LOAD_CURRENT'][$field_table]['fid']) ? $info['storage']['details']['sql']['FIELD_LOAD_CURRENT'][$field_table]['fid'] : NULL;
        if ($field_table && $field_column) {
          $query = db_select($field_table, 'f')
            ->fields('f', array(
            $field_column,
          ))
            ->execute();
          $files = $files + $query
            ->fetchAllKeyed(0, 0);
        }
        else {
          drupal_set_message(t('You are using non-sql storage for the field :field. These files will need to be manually moved and files_managed updated.', array(
            ':field' => $field,
          )), 'error');
        }
      }
    }
  }
}

/**
 * Updates webform file component settings to be secure.
 *
 * @param array $files
 *   Passed by reference to collect fids that may require moving on server.
 */
function file_upload_security_fix_webforms(&$files = array()) {
  $query = db_select('webform_component', 'c')
    ->fields('c', array(
    'cid',
  ))
    ->condition('extra', '%s:6:"scheme";s:6:"public"%', 'LIKE')
    ->condition('type', 'file');
  $query
    ->leftJoin('webform_submitted_data', 's', 's.cid = c.cid AND s.nid = c.nid');
  $query
    ->fields('s', array(
    'data',
  ));
  $query
    ->isNotNull('data');
  $fids = $query
    ->execute()
    ->fetchAllKeyed(1, 1);
  if ($fids) {
    $files = $files + $fids;
  }
  $update = db_update('webform_component')
    ->expression('extra', 'REPLACE(extra, :public_scheme, :private_scheme)', array(
    ':public_scheme' => 's:6:"scheme";s:6:"public"',
    ':private_scheme' => 's:6:"scheme";s:7:"private"',
  ))
    ->condition('type', 'file')
    ->execute();
}

/**
 * Batch setter to move an array of files into the private system.
 *
 * @param array $files
 *   An array of file ids.
 */
function file_upload_security_move_fixed_files($files = array()) {
  if ($files) {
    $batch = array(
      'operations' => array(),
      'finished' => 'file_upload_security_fix_finished',
      'title' => t('Move files into secure area'),
      'init_message' => t('Moving existing files into secure storage.'),
      'progress_message' => t('Processed @current out of @total'),
      'error_message' => t('There has been an error moving files'),
    );
    $iterator = new RecursiveArrayIterator($files);
    while ($iterator
      ->valid()) {
      $fid = $iterator
        ->current();
      if ($fid) {
        $batch['operations'][] = array(
          'file_upload_security_fix_move_file_batch',
          array(
            $fid,
          ),
        );
      }
      $iterator
        ->next();
    }
    if ($batch['operations']) {
      batch_set($batch);
      batch_process('admin/config/media/file_upload_security');
    }
  }
}

/**
 * Batch operation to move a file into the private area.
 *
 * @param int $fid
 *   A valid Drupal file id.
 * @param array $context
 *   A batch context array.
 */
function file_upload_security_fix_move_file_batch($fid, &$context) {
  $context['message'] = t('Moving file !fid', array(
    '!fid' => $fid,
  ));
  $success = FALSE;
  if ($fid && ($file = file_load($fid))) {
    if (isset($file->uri)) {
      $uri = str_replace('public://', 'private://', $file->uri);
      $success = file_move($file, $uri);
      if ($success) {
        $context['results'][] = $fid;
      }
    }
  }
  if (!$success) {
    $context['results']['error'][] = $fid;
    watchdog('file_upload_security', 'File :fid does not exist.', array(
      ':fid' => $fid,
    ));
  }
}

/**
 * A batch finished operation.
 *
 * @param bool $success
 *   Whether the batch completed without fatal error.
 * @param array $results
 *   An array of the batch operation results.
 * @param array $operations
 *   An array of any error messages set.
 */
function file_upload_security_fix_finished($success, $results, $operations) {
  if (!$success || array_key_exists('error', $results) && $results['error']) {
    if (!$operations) {
      $count = count($results['error']);
      unset($results['error']);
      $success_count = count($results);
      if ($success_count) {
        drupal_set_message(t(':success files were successfully moved. :count files were not moved - please check your logs for errors.', array(
          ':success' => $success_count,
          ':count' => $count,
        )));
      }
      drupal_set_message(t(':count files were not moved - please check your logs for errors.', array(
        ':count' => $count,
      )));
    }
    else {
      $error_operation = reset($operations);
      $message = t('An error occurred while processing %error_operation with arguments: @arguments', array(
        '%error_operation' => $error_operation[0],
        '@arguments' => print_r($error_operation[1], TRUE),
      ));
      drupal_set_message($message, 'error');
    }
  }
  else {
    $count = count($results);
    drupal_set_message(t(':count files were successfully moved.', array(
      ':count' => $count,
    )));
  }
}

/**
 * Helper to return an array of entities user 0 can create.
 *
 * @return array
 *   An array of entity types.
 */
function file_upload_security_affected_types() {
  $anonymous_entities =& drupal_static(__FUNCTION__);
  if (!isset($anonymous_entities)) {
    $anonymous_entities = array(
      'fields' => array(),
      'entities' => array(),
    );
    $file_fields = file_upload_security_affected_fields();
    if ($file_fields['entities']) {
      $all_entities = entity_get_info();
      $affected_entities = array_intersect_key($all_entities, $file_fields['entities']);
      $account = user_load(0);
      $iterator = new RecursiveArrayIterator($affected_entities);
      while ($iterator
        ->valid()) {
        $data = $iterator
          ->current();
        $type = $iterator
          ->key();
        if ($data) {
          if (array_key_exists('bundles', $data) && $data['bundles']) {
            foreach ($data['bundles'] as $bundle => $array) {
              if (in_array($bundle, $file_fields['entities'][$type])) {
                $bundle_key = $data['entity keys']['bundle'];
                $entity = $bundle_key ? entity_create($type, array(
                  $bundle_key => $bundle,
                )) : $bundle;
                if (entity_access('create', $type, $entity, $account)) {
                  $anonymous_entities['entities'][$type][] = $bundle;
                  foreach ($file_fields['fields'] as $field => $bundles) {
                    if (in_array($bundle, $bundles)) {
                      $field_info = field_info_field($field);
                      if ($field_info && field_access('create', $field_info, $bundle, $entity, $account)) {
                        $anonymous_entities['fields'][$field][] = $bundle;
                      }
                    }
                  }
                }
              }
            }
          }
        }
        $iterator
          ->next();
      }
    }
  }
  return $anonymous_entities;
}

/**
 * Helper to get all file fields in use on site.
 *
 * @return array
 *   An array of file fields and entities using them by bundle.
 */
function file_upload_security_affected_fields() {
  $file_fields =& drupal_static(__FUNCTION__);
  if (!isset($file_fields)) {
    $fields = field_info_field_map();
    $file_fields = array(
      'fields' => array(),
      'entities' => array(),
    );
    if ($fields) {
      $iterator = new RecursiveArrayIterator($fields);
      while ($iterator
        ->valid()) {
        $data = $iterator
          ->current();
        $field = $iterator
          ->key();
        if (array_key_exists('type', $data) && in_array($data['type'], file_upload_security_field_types())) {
          if (array_key_exists('bundles', $data)) {
            $bundles = reset($data['bundles']);
            $entity = key($data['bundles']);
            foreach ($bundles as $bundle) {
              $file_fields['fields'][$field][] = $bundle;
            }
            if (array_key_exists($entity, $file_fields['entities'])) {
              $file_fields['entities'][$entity] = array_merge($file_fields['entities'][$entity], $bundles);
            }
            else {
              $file_fields['entities'][$entity] = $bundles;
            }
          }
        }
        $iterator
          ->next();
      }
    }
  }
  return $file_fields;
}

/**
 * Helper to return the types of feeds to protect.
 *
 * @return array
 *   An array of field names.
 */
function file_upload_security_field_types() {
  return array(
    'file',
    'image',
  );
}

Functions

Namesort descending Description
file_upload_security_affected_fields Helper to get all file fields in use on site.
file_upload_security_affected_types Helper to return an array of entities user 0 can create.
file_upload_security_amend_widget_settings Helper to carry out the file_upload_security action based on settings.
file_upload_security_field_types Helper to return the types of feeds to protect.
file_upload_security_fix_fields Updates field settings to be secure.
file_upload_security_fix_files Helper to move files in insecure locations to the private file system.
file_upload_security_fix_files_drush Avoid use of batches when fixing files via drush.
file_upload_security_fix_finished A batch finished operation.
file_upload_security_fix_move_file_batch Batch operation to move a file into the private area.
file_upload_security_fix_webforms Updates webform file component settings to be secure.
file_upload_security_form_field_ui_field_edit_form_alter Implements hook_form_FORM_ID_alter().
file_upload_security_form_field_ui_field_settings_form_alter Implements hook_form_FORM_ID_alter().
file_upload_security_form_form_builder_field_configure_alter Implements hook_form_FORM_ID_alter().
file_upload_security_form_webform_component_edit_form_alter Implements hook_form_FORM_ID_alter().
file_upload_security_is_affected Helper to discover is a given entity bundle/combination is affected.
file_upload_security_menu Implements hook_menu().
file_upload_security_move_fixed_files Batch setter to move an array of files into the private system.
file_upload_security_permission Implements hook_permission().
file_upload_security_set_message Helper to set a message about protecting the file structure.
file_upload_security_webform_component_presave Implements hook_webform_component_presave().

Constants

Namesort descending Description
FILE_UPLOAD_SECURITY_PROTECT
FILE_UPLOAD_SECURITY_PSA
FILE_UPLOAD_SECURITY_WARN @file Helper module to advise and resolve security issues in file uploads.