You are here

class ExecutablePhp in Security Review 8

Checks if PHP files written to the files directory can be executed.

Hierarchy

Expanded class hierarchy of ExecutablePhp

1 file declares its use of ExecutablePhp
security_review.module in ./security_review.module
Site security review and reporting Drupal module.

File

src/Checks/ExecutablePhp.php, line 14

Namespace

Drupal\security_review\Checks
View source
class ExecutablePhp extends Check {

  /**
   * Drupal's HTTP Client.
   *
   * @var \Drupal\Core\Http\Client
   */
  protected $httpClient;

  /**
   * {@inheritdoc}
   */
  public function __construct() {
    parent::__construct();
    $this->httpClient = $this->container
      ->get('http_client');
  }

  /**
   * {@inheritdoc}
   */
  public function getNamespace() {
    return 'Security Review';
  }

  /**
   * {@inheritdoc}
   */
  public function getTitle() {
    return 'Executable PHP';
  }

  /**
   * {@inheritdoc}
   */
  public function run($cli = FALSE) {
    global $base_url;
    $result = CheckResult::SUCCESS;
    $findings = [];

    // Set up test file data.
    $message = 'Security review test ' . date('Ymdhis');
    $content = "<?php\necho '" . $message . "';";
    $file_path = PublicStream::basePath() . '/security_review_test.php';

    // Create the test file.
    if ($test_file = @fopen('./' . $file_path, 'w')) {
      fwrite($test_file, $content);
      fclose($test_file);
    }

    // Try to access the test file.
    try {
      $response = $this->httpClient
        ->get($base_url . '/' . $file_path);
      if ($response
        ->getStatusCode() == 200 && $response
        ->getBody() === $message) {
        $result = CheckResult::FAIL;
        $findings[] = 'executable_php';
      }
    } catch (RequestException $e) {

      // Access was denied to the file.
    }

    // Remove the test file.
    if (file_exists('./' . $file_path)) {
      @unlink('./' . $file_path);
    }

    // Check for presence of the .htaccess file and if the contents are correct.
    $htaccess_path = PublicStream::basePath() . '/.htaccess';
    if (!file_exists($htaccess_path)) {
      $result = CheckResult::FAIL;
      $findings[] = 'missing_htaccess';
    }
    else {

      // Check whether the contents of .htaccess are correct.
      $contents = file_get_contents($htaccess_path);
      $expected = FileSecurity::htaccessLines(FALSE);

      // Trim each line separately then put them back together.
      $contents = implode("\n", array_map('trim', explode("\n", trim($contents))));
      $expected = implode("\n", array_map('trim', explode("\n", trim($expected))));
      if ($contents !== $expected) {
        $result = CheckResult::FAIL;
        $findings[] = 'incorrect_htaccess';
      }

      // Check whether .htaccess is writable.
      if (!$cli) {
        $writable_htaccess = is_writable($htaccess_path);
      }
      else {
        $writable = $this
          ->security()
          ->findWritableFiles([
          $htaccess_path,
        ], TRUE);
        $writable_htaccess = !empty($writable);
      }
      if ($writable_htaccess) {
        $findings[] = 'writable_htaccess';
        if ($result !== CheckResult::FAIL) {
          $result = CheckResult::WARN;
        }
      }
    }
    return $this
      ->createResult($result, $findings);
  }

  /**
   * {@inheritdoc}
   */
  public function runCli() {
    return $this
      ->run(TRUE);
  }

  /**
   * {@inheritdoc}
   */
  public function help() {
    $paragraphs = [];
    $paragraphs[] = $this
      ->t('The Drupal files directory is for user-uploaded files and by default provides some protection against a malicious user executing arbitrary PHP code against your site.');
    $paragraphs[] = $this
      ->t('Read more about the <a href="https://drupal.org/node/615888">risk of PHP code execution on Drupal.org</a>.');
    return [
      '#theme' => 'check_help',
      '#title' => $this
        ->t('Executable PHP in files directory'),
      '#paragraphs' => $paragraphs,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function evaluate(CheckResult $result) {
    $paragraphs = [];
    foreach ($result
      ->findings() as $label) {
      switch ($label) {
        case 'executable_php':
          $paragraphs[] = $this
            ->t('Security Review was able to execute a PHP file written to your files directory.');
          break;
        case 'missing_htaccess':
          $directory = PublicStream::basePath();
          $paragraphs[] = $this
            ->t("The .htaccess file is missing from the files directory at @path", [
            '@path' => $directory,
          ]);
          $paragraphs[] = $this
            ->t("Note, if you are using a webserver other than Apache you should consult your server's documentation on how to limit the execution of PHP scripts in this directory.");
          break;
        case 'incorrect_htaccess':
          $paragraphs[] = $this
            ->t("The .htaccess file exists but does not contain the correct content. It is possible it's been maliciously altered.");
          break;
        case 'writable_htaccess':
          $paragraphs[] = $this
            ->t("The .htaccess file is writable which poses a risk should a malicious user find a way to execute PHP code they could alter the .htaccess file to allow further PHP code execution.");
          break;
      }
    }
    return [
      '#theme' => 'check_evaluation',
      '#paragraphs' => $paragraphs,
      '#items' => [],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function evaluatePlain(CheckResult $result) {
    $paragraphs = [];
    $directory = PublicStream::basePath();
    foreach ($result
      ->findings() as $label) {
      switch ($label) {
        case 'executable_php':
          $paragraphs[] = $this
            ->t('PHP file executed in @path', [
            '@path' => $directory,
          ]);
          break;
        case 'missing_htaccess':
          $paragraphs[] = $this
            ->t('.htaccess is missing from @path', [
            '@path' => $directory,
          ]);
          break;
        case 'incorrect_htaccess':
          $paragraphs[] = $this
            ->t('.htaccess wrong content');
          break;
        case 'writable_htaccess':
          $paragraphs[] = $this
            ->t('.htaccess writable');
          break;
      }
    }
    return implode("\n", $paragraphs);
  }

  /**
   * {@inheritdoc}
   */
  public function getMessage($result_const) {
    switch ($result_const) {
      case CheckResult::SUCCESS:
        return $this
          ->t('PHP files in the Drupal files directory cannot be executed.');
      case CheckResult::FAIL:
        return $this
          ->t('PHP files in the Drupal files directory can be executed.');
      case CheckResult::WARN:
        return $this
          ->t('The .htaccess file in the files directory is writable.');
      default:
        return $this
          ->t('Unexpected result.');
    }
  }

}

Members

Namesort descending Modifiers Type Description Overrides
Check::$config protected property The configuration storage for this check.
Check::$container protected property The service container.
Check::$settings protected property Settings handler for this check.
Check::$state protected property The State system.
Check::$statePrefix protected property The check's prefix in the State system.
Check::checklist protected function Returns the Security Review Checklist service.
Check::configFactory protected function Returns the Config factory.
Check::container protected function Returns the service container.
Check::createResult public function Creates a new CheckResult for this Check.
Check::currentUser protected function Returns the current Drupal user.
Check::database protected function Returns the database connection.
Check::enable public function Enables the check. Has no effect if the check was not skipped.
Check::entityTypeManager protected function Returns the entity type manager.
Check::getMachineNamespace public function Returns the namespace of the check.
Check::getMachineTitle public function Returns the machine name of the check. 5
Check::id final public function Returns the identifier constructed using the namespace and title values.
Check::isSkipped public function Returns whether the check is skipped. Checks are not skipped by default.
Check::kernel protected function Returns the Drupal Kernel.
Check::lastResult public function Returns the last stored result of the check.
Check::lastRun public function Returns the timestamp the check was last run.
Check::moduleHandler protected function Returns the module handler.
Check::security protected function Returns the Security Review Security service.
Check::securityReview protected function Returns the Security Review service.
Check::settings public function Returns the check-specific settings' handler.
Check::skip public function Marks the check as skipped.
Check::skippedBy public function Returns the user the check was skipped by.
Check::skippedOn public function Returns the timestamp the check was last skipped on.
Check::storeResult public function Stores a result in the state system.
Check::storesFindings public function Returns whether the findings should be stored or reproduced when needed. 2
DependencySerializationTrait::$_entityStorages protected property An array of entity type IDs keyed by the property name of their storages.
DependencySerializationTrait::$_serviceIds protected property An array of service IDs keyed by property name used for serialization.
DependencySerializationTrait::__sleep public function 1
DependencySerializationTrait::__wakeup public function 2
ExecutablePhp::$httpClient protected property Drupal's HTTP Client.
ExecutablePhp::evaluate public function Returns the evaluation page of a result. Overrides Check::evaluate
ExecutablePhp::evaluatePlain public function Evaluates a CheckResult and returns a plaintext output. Overrides Check::evaluatePlain
ExecutablePhp::getMessage public function Converts a result integer to a human-readable result message. Overrides Check::getMessage
ExecutablePhp::getNamespace public function Returns the namespace of the check. Overrides Check::getNamespace
ExecutablePhp::getTitle public function Returns the human-readable title of the check. Overrides Check::getTitle
ExecutablePhp::help public function Returns the check-specific help page. Overrides Check::help
ExecutablePhp::run public function The actual procedure of carrying out the check. Overrides Check::run
ExecutablePhp::runCli public function Same as run(), but used in CLI context such as Drush. Overrides Check::runCli
ExecutablePhp::__construct public function Initializes the configuration storage and the settings handler. Overrides Check::__construct
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.