Problem/Motivation

As part of the general clean-up of the File API, file_save_htaccess() and file_ensure_htaccess() need to be deprecated and moved into services.

Proposed Resolution

Create a new service 'file.htaccess_writer' and deprecate file_ensure_htaccess() in favour of \Drupal\Core\File\HtaccessWriterInterface::ensure()

Deprecate file_save_htaccess() in favour of the FileSecurity component added in #3013210: Add drupal/core-filesecurity component for writing htaccess files

CommentFileSizeAuthor
#175 interdiff-2620304-163-175.txt1.67 KBmcdruid
#175 2620304-175.patch31.67 KBmcdruid
#163 2620304-163-interdiff.txt666 byteskim.pepper
#163 2620304-163.patch30.67 KBkim.pepper
#161 2620304-161-interdiff.txt3.09 KBkim.pepper
#161 2620304-161.patch30.7 KBkim.pepper
#159 2620304-159-interdiff.txt930 byteskim.pepper
#159 2620304-159.patch30.29 KBkim.pepper
#156 2620304-156-interdiff.txt20.44 KBkim.pepper
#156 2620304-156.patch30.05 KBkim.pepper
#153 2620304-153-interdiff.txt994 byteskim.pepper
#153 2620304-153.patch24.86 KBkim.pepper
#152 2620304-152-interdiff.txt2.76 KBkim.pepper
#152 2620304-152.patch24.88 KBkim.pepper
#150 2620304-150-interdiff.txt25.27 KBkim.pepper
#150 2620304-150.patch25.27 KBkim.pepper
#144 interdiff-2620304-140-144.txt775 bytesmcdruid
#144 2620304-144.patch26.04 KBmcdruid
#140 2620304-140.patch25.35 KBkim.pepper
#131 2620304-131-interdiff.txt2.23 KBkim.pepper
#131 2620304-131.patch67.06 KBkim.pepper
#128 2620304-128-interdiff.txt44.67 KBkim.pepper
#128 2620304-128.patch66.67 KBkim.pepper
#124 2620304-124-interdiff.txt1.16 KBkim.pepper
#124 2620304-124.patch25.35 KBkim.pepper
#120 2620304-118.patch25.38 KBkim.pepper
#118 2620304-118-interdiff.txt954 byteskim.pepper
#118 2620304-118.patch25.55 KBkim.pepper
#116 2620304-116-interdiff.txt672 byteskim.pepper
#116 2620304-116.patch25.41 KBkim.pepper
#114 2620304-114-interdiff.txt652 byteskim.pepper
#114 2620304-114.patch25.41 KBkim.pepper
#112 2620304-112.patch25.39 KBkim.pepper
#110 2620304-110-interdiff.txt2.97 KBkim.pepper
#110 2620304-110.patch25.91 KBkim.pepper
#108 2620304-108.patch25.67 KBkim.pepper
#106 2620304-106-interdiff.txt3.59 KBkim.pepper
#106 2620304-106.patch25.5 KBkim.pepper
#105 2620304-105.patch25.5 KBkim.pepper
#103 2620304-103.patch25.52 KBkim.pepper
#97 2620304-htaccess-97.patch25.52 KBkim.pepper
#97 2620304-htaccess-97-interdiff.txt1.32 KBkim.pepper
#96 2620304-htaccess-95.patch26.33 KBkim.pepper
#96 2620304-htaccess-95-interdiff.txt9.46 KBkim.pepper
#93 2620304-htaccess-93.patch26.21 KBkim.pepper
#93 2620304-htaccess-93-interdiff.txt2.21 KBkim.pepper
#91 2620304-htaccess-91.patch26.72 KBkim.pepper
#91 2620304-htaccess-91-interdiff.txt2.5 KBkim.pepper
#88 2620304-88.patch25 KBkostyashupenko
#83 2620304-83.patch25.04 KBjibran
#83 interdiff-d522cd.txt638 bytesjibran
#65 2620304-65.patch25.23 KBalexpott
#65 60-65-interdiff.txt5.16 KBalexpott
#60 interdiff-2620304-58-60.txt3.89 KBphenaproxima
#60 2620304-60.patch26.94 KBphenaproxima
#58 2620304-htaccess-57.patch26.04 KBkim.pepper
#58 2620304-htaccess-57-interdiff.txt4.27 KBkim.pepper
#56 2620304-htaccess-56.patch26.04 KBandypost
#56 interdiff.txt631 bytesandypost
#55 2620304-htaccess-55.patch26.65 KBkim.pepper
#55 2620304-htaccess-55-interdiff.txt2.39 KBkim.pepper
#53 2620304-htaccess-53.patch26.91 KBkim.pepper
#53 2620304-htaccess-53-interdiff.txt5.9 KBkim.pepper
#48 2620304-htaccess-48.patch22.72 KBkim.pepper
#48 2620304-htaccess-48-interdiff.txt9.71 KBkim.pepper
#45 2620304-htaccess-45.patch24.53 KBkim.pepper
#45 2620304-htaccess-45-interdiff.txt427 byteskim.pepper
#43 2620304-htaccess-43.patch24.94 KBkim.pepper
#43 2620304-htaccess-43-interdiff.txt9.99 KBkim.pepper
#40 2620304-htaccess-40.patch29.86 KBkim.pepper
#40 2620304-htaccess-40-interdiff.txt1.54 KBkim.pepper
#38 2620304-htaccess-38.patch29.45 KBkim.pepper
#38 2620304-htaccess-38-interdiff.txt19.16 KBkim.pepper
#35 2620304-htaccess-34.patch23.21 KBkim.pepper
#35 2620304-htaccess-34-interdiff.txt2.45 KBkim.pepper
#32 2620304-htaccess-interdiff-32.txt6.56 KBkim.pepper
#31 2620304-htaccess-31.patch22.6 KBkim.pepper
#31 2620304-htaccess-31-interdiff.txt214.52 KBkim.pepper
#30 2620304-htaccess-30.patch236.5 KBkim.pepper
#30 2620304-htaccess-30-interdiff.txt220.46 KBkim.pepper
#28 2620304-htaccess-28.patch21.33 KBkim.pepper
#28 2620304-htaccess-28-interdiff.txt21.33 KBkim.pepper
#22 2620304-htaccess-22.patch20.66 KBandypost
#22 2620304-interdiff-21.txt961 bytesandypost
#21 2620304-htaccess-21.patch20.47 KBandypost
#21 2620304-interdiff-19.txt4.36 KBandypost
#19 2620304-htaccess-19.patch16.86 KBandypost
#19 2620304-interdiff-15.txt3.37 KBandypost
#17 2620304-17.patch5.13 KBchiranjeeb2410
#15 interdiff-2620304-12-14.txt7.54 KBstarshaped
#15 2620304-14.patch16.35 KBstarshaped
#12 2620304-htaccess-12.patch8.69 KBandypost
#10 convert-2620304-10.patch3.24 KBnickolaj
#5 2620304-5.patch8.17 KBphenaproxima
#2 2620304-2.patch5.8 KBphenaproxima

Comments

phenaproxima created an issue. See original summary.

phenaproxima’s picture

Status: Active » Needs review
Issue tags: +Needs tests
StatusFileSize
new5.8 KB

Pretty basic...

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.0-beta1 was released on March 2, 2016, which means new developments and disruptive changes should now be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

phenaproxima’s picture

StatusFileSize
new8.17 KB

Rerolled against 8.3.x.

Status: Needs review » Needs work

The last submitted patch, 5: 2620304-5.patch, failed testing.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

nickolaj’s picture

Status: Needs work » Needs review
Issue tags: +KharkivGSW18
StatusFileSize
new3.24 KB

rework against current core, no interdiff cos lots of changes

andypost’s picture

Status: Needs review » Needs work

Last patch have only part of previous

andypost’s picture

Status: Needs work » Needs review
StatusFileSize
new8.69 KB

Proper patch, also added missing doc blocks, still needs tests for new service & exception?!

andypost’s picture

Issue tags: +Needs change record
+++ b/core/includes/file.inc
@@ -354,29 +335,21 @@ function file_ensure_htaccess() {
+  @trigger_error('file_save_htaccess() is deprecated in Drupal 8.6.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\File\Htaccess::save(). See https://www.drupal.org/node/.', E_USER_DEPRECATED);

and needs CR

Status: Needs review » Needs work

The last submitted patch, 12: 2620304-htaccess-12.patch, failed testing. View results

starshaped’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
StatusFileSize
new16.35 KB
new7.54 KB

@phenaproxima and I worked on this. Rather than write new tests, we adapted existing test of file_save_htaccess and added test coverage of the deprecation notices.

Status: Needs review » Needs work

The last submitted patch, 15: 2620304-14.patch, failed testing. View results

chiranjeeb2410’s picture

Status: Needs work » Needs review
StatusFileSize
new5.13 KB

Complete reroll for patch in #2 against latest core. Hope to get a green flag.

Status: Needs review » Needs work

The last submitted patch, 17: 2620304-17.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

andypost’s picture

Status: Needs work » Needs review
Issue tags: -Needs change record
StatusFileSize
new3.37 KB
new16.86 KB

While working on CR https://www.drupal.org/node/2940126, I found that https://www.drupal.org/node/2418133 does not mentions about conversion of file_htaccess_lines() probably there was a reason for it... fixed

Here's updated patch adds ref to CR & fix most of failures

@chiranjeeb2410 please don't hijack issue patches

Status: Needs review » Needs work

The last submitted patch, 19: 2620304-htaccess-19.patch, failed testing. View results

andypost’s picture

Status: Needs work » Needs review
StatusFileSize
new4.36 KB
new20.47 KB

Fix test failures by replacing usage, added todo and issue #2940135: Use file_system service to \Drupal\Core\Config\FileStorage

andypost’s picture

StatusFileSize
new961 bytes
new20.66 KB

a bit of clean-up

The last submitted patch, 21: 2620304-htaccess-21.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 22: 2620304-htaccess-22.patch, failed testing. View results

robpowell’s picture

working on this for SprintWeekend2018

robpowell’s picture

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

kim.pepper’s picture

Status: Needs work » Needs review
StatusFileSize
new21.33 KB
new21.33 KB

Re-roll of #22

Status: Needs review » Needs work

The last submitted patch, 28: 2620304-htaccess-28.patch, failed testing. View results

kim.pepper’s picture

Status: Needs work » Needs review
StatusFileSize
new220.46 KB
new236.5 KB

Fixes a few tests.

kim.pepper’s picture

StatusFileSize
new214.52 KB
new22.6 KB

Oops! Didn't mean to post my local composer changes!

kim.pepper’s picture

StatusFileSize
new6.56 KB

Here's the correct interdiff between #31 and #28

The last submitted patch, 30: 2620304-htaccess-30.patch, failed testing. View results

andypost’s picture

+++ b/core/tests/Drupal/KernelTests/Core/File/HtaccessTest.php
@@ -3,48 +3,92 @@
+ * @coversDefaultClass \Drupal\Core\File\Htaccess
  * @group File
+ * @group legacy
...
+  public function testDeprecatedFunctions() {
+    file_save_htaccess($this->public);
+    file_ensure_htaccess();

It looks that test needs split or only testDeprecatedFunctions() should be marked as @legacy

kim.pepper’s picture

StatusFileSize
new2.45 KB
new23.21 KB

Thanks. Fixes for #34

andypost’s picture

Status: Needs review » Reviewed & tested by the community

Let's get commiter's piv, imo good to go

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/core.services.yml
    @@ -1644,6 +1647,9 @@ services:
    +  file.htaccess:
    

    I think a better service name would be file.htaccess_writer or even core.htaccess_writer as that's the main purpose. Maybe someone else has better suggestions but file.htaccess does not seem that great to me.

  2. +++ b/core/core.services.yml
    @@ -1644,6 +1647,9 @@ services:
    +    class: Drupal\Core\File\Htaccess
    

    HtaccessWriter I think is a better name.

  3. +++ b/core/lib/Drupal/Core/File/Htaccess.php
    @@ -0,0 +1,118 @@
    +    $htaccess_lines = FileStorage::htaccessLines($private);
    

    Let's move \Drupal\Component\PhpStorage\FileStorage::htaccessLines() to here and deprecate that. It was only put there to have it in an autoloadable class and this is a better home.

  4. I'm not sure about the new exception and the behaviour change. Ie. file_save_htaccess() writes an error to the log but the new service::save() does not. It throws an exception. Combining functional change with refactoring to a service feels wrong to me.
kim.pepper’s picture

Status: Needs work » Needs review
StatusFileSize
new19.16 KB
new29.45 KB

Thanks for the review!

  1. Fixed
  2. Fixed
  3. Fixed
  4. I moved the logging call inside the HtAccessWriter::save() method so its BC. Only new usages of the service will need to handle the exception. Does this address the issue sufficiently?

Status: Needs review » Needs work

The last submitted patch, 38: 2620304-htaccess-38.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

kim.pepper’s picture

Status: Needs work » Needs review
StatusFileSize
new1.54 KB
new29.86 KB

Hmm. Seems that moving the \Drupal\Component\PhpStorage\FileStorage::htaccessLines to \Drupal\Core\File\HtaccessWriter::htaccessLines is causing a fail because we calling code Drupal\Core from within a Drupal\Component.

Any thoughts on how to resolve that apart from duplicating the code?

Status: Needs review » Needs work

The last submitted patch, 40: 2620304-htaccess-40.patch, failed testing. View results

alexpott’s picture

@kim.pepper I think we should continue with calling \Drupal\Component\PhpStorage\FileStorage::htaccessLines() then and file a follow-up to try to resolve this. The other answer is that we should try to make the HtAccessWriter a component but that is going to be nigh on impossible. For me this points to the fact that PhpStorage doesn't really feel like a component either.

Maybe we can find some other similar stuff and make a security component that has this ¯\_(ツ)_/¯

kim.pepper’s picture

Status: Needs work » Needs review
StatusFileSize
new9.99 KB
new24.94 KB

I've reverted the htaccess() move and created follow up #3013210: Add drupal/core-filesecurity component for writing htaccess files.

Status: Needs review » Needs work

The last submitted patch, 43: 2620304-htaccess-43.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

kim.pepper’s picture

Status: Needs work » Needs review
StatusFileSize
new427 bytes
new24.53 KB

Remove unused import.

jibran’s picture

Status: Needs review » Reviewed & tested by the community

#37 is addressed so back to RTBC.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/lib/Drupal/Core/File/HtaccessWriter.php
@@ -0,0 +1,122 @@
+    catch (\Exception $e) {
+      $this->logger->error($e->getMessage());
+      return FALSE;
+    }
...
+      $this->logger->error("Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: <pre><code>@htaccess

", [
+ '%directory' => $directory,
+ '@htaccess' => $htaccess_lines,
+ ]);
+ throw new HtaccessNotWriteableException($directory, $htaccess_lines);

This will result in duplicate errors logged. Why do we want this to throw an exception? If we feel that that is the better architecture then I guess that we could make that change here but it feels an unnecessary change. We need on some level to justify this change.

kim.pepper’s picture

Status: Needs work » Needs review
StatusFileSize
new9.71 KB
new22.72 KB

Re #47 I've removed the exception.

I also added an interface.

jibran’s picture

Status: Needs review » Reviewed & tested by the community

Interface additions is good. Back to RTBC as #47 is addressed.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

One thing I'm asking myself is does this need to be its own service. One thought was to add this to FileSystem but OTOH this is really a HTTP server detail. The next question I asked myself is have we moved all the htaccess logic possible to the new service and I think there's something we're missing. I think it might be worth moving much of the code from system_requirements() here. So that checking and ensuring is done in the same place. Perhaps there is other htaccess related code dotted about the system that could be moved here.

Here's the current code:

  // Test the contents of the .htaccess files.
  if ($phase == 'runtime') {
    // Try to write the .htaccess files first, to prevent false alarms in case
    // (for example) the /tmp directory was wiped.
    file_ensure_htaccess();
    $file_system = \Drupal::service('file_system');
    $htaccess_files['public://.htaccess'] = [
      'title' => t('Public files directory'),
      'directory' => $file_system->realpath('public://'),
    ];
    if (PrivateStream::basePath()) {
      $htaccess_files['private://.htaccess'] = [
        'title' => t('Private files directory'),
        'directory' => $file_system->realpath('private://'),
      ];
    }
    $htaccess_files['temporary://.htaccess'] = [
      'title' => t('Temporary files directory'),
      'directory' => $file_system->realpath('temporary://'),
    ];
    foreach ($htaccess_files as $htaccess_file => $info) {
      // Check for the string which was added to the recommended .htaccess file
      // in the latest security update.
      if (!file_exists($htaccess_file) || !($contents = @file_get_contents($htaccess_file)) || strpos($contents, 'Drupal_Security_Do_Not_Remove_See_SA_2013_003') === FALSE) {
        $url = 'https://www.drupal.org/SA-CORE-2013-003';
        $requirements[$htaccess_file] = [
          'title' => $info['title'],
          'value' => t('Not fully protected'),
          'severity' => REQUIREMENT_ERROR,
          'description' => t('See <a href=":url">@url</a> for information about the recommended .htaccess file which should be added to the %directory directory to help protect against arbitrary code execution.', [':url' => $url, '@url' => $url, '%directory' => $info['directory']]),
        ];
      }
    }
  }

We could move this into the new service as something like:

  public function checkRequirements() {
    $requirements = [];
    $htaccess_files['public://.htaccess'] = [
      'title' => new TranlatableMarkup('Public files directory'),
      'directory' => $this->fileSystem->realpath('public://'),
    ];
    if (PrivateStream::basePath()) {
      $htaccess_files['private://.htaccess'] = [
        'title' => new TranlatableMarkup('Private files directory'),
        'directory' => $this->fileSystem->realpath('private://'),
      ];
    }
    $htaccess_files['temporary://.htaccess'] = [
      'title' => new TranlatableMarkup('Temporary files directory'),
      'directory' => $this->fileSystem->realpath('temporary://'),
    ];
    foreach ($htaccess_files as $htaccess_file => $info) {
      // Check for the string which was added to the recommended .htaccess file
      // in the latest security update.
      if (!file_exists($htaccess_file) || !($contents = @file_get_contents($htaccess_file)) || strpos($contents, 'Drupal_Security_Do_Not_Remove_See_SA_2013_003') === FALSE) {
        $url = 'https://www.drupal.org/SA-CORE-2013-003';
        $requirements[$htaccess_file] = [
          'title' => $info['title'],
          'value' =>  new TranlatableMarkup('Not fully protected'),
          'severity' => REQUIREMENT_ERROR,
          'description' =>  new TranlatableMarkup('See <a href=":url">@url</a> for information about the recommended .htaccess file which should be added to the %directory directory to help protect against arbitrary code execution.', [':url' => $url, '@url' => $url, '%directory' => $info['directory']]),
        ];
      }
    }
    return $requirements;
  }

And maybe even refactor the list of directories into a protected method to be shared by the ensure method - so that list is always obtained from the same place.

andypost’s picture

@alexpott requirements sounds like follow-up and could be merged with #2909480: Move REQUIREMENT_* constants out of install.inc file

alexpott’s picture

@andypost there thing is we're introducing a new interface so now is a good time to get it right.

kim.pepper’s picture

Status: Needs work » Needs review
StatusFileSize
new5.9 KB
new26.91 KB

#50 sounds like a nice cleanup.

andypost’s picture

+++ b/core/lib/Drupal/Core/File/HtaccessWriter.php
@@ -25,6 +26,17 @@ class HtaccessWriter implements HtaccessWriterInterface {
+  protected $htaccessFiles;

@@ -102,4 +114,55 @@ public function save($directory, $private = TRUE, $force_overwrite = FALSE) {
+    foreach ($this->htaccessFiles() as $htaccessFile => $info) {
...
+  protected function htaccessFiles() {
+    $this->htaccessFiles['public://.htaccess'] = [

Any reason to use protected var if you always call a method?

kim.pepper’s picture

StatusFileSize
new2.39 KB
new26.65 KB

Ah good point!

I've also re-used the htaccessFiles() method in HtaccessWriter::save() as @alexpott suggested in #50

andypost’s picture

Status: Needs review » Reviewed & tested by the community
StatusFileSize
new631 bytes
new26.04 KB

Removal of unneeded changes

andypost’s picture

+++ b/core/lib/Drupal/Core/File/HtaccessWriter.php
@@ -0,0 +1,158 @@
+  public function checkRequirements() {
...
+        $requirements[$htaccessFile] = [
+          'title' => $info['title'],
+          'value' => new TranslatableMarkup('Not fully protected'),
...
+    return $requirements;

Naming... maybe better to call it getter...

kim.pepper’s picture

StatusFileSize
new4.27 KB
new26.04 KB

Fixes #57

Also changed variables to camelCase for consistency.

larowlan’s picture

Status: Reviewed & tested by the community » Needs work

Looks good, couple of observations

  1. +++ b/core/includes/install.core.inc
    @@ -1127,7 +1127,7 @@ function install_base_system(&$install_state) {
       // the install_verify_requirements() task. Note that we cannot call
       // file_ensure_access() any earlier than this, since it relies on
       // system.module in order to work.
    

    this comment is now outdated

  2. +++ b/core/lib/Drupal/Core/Config/FileStorage.php
    @@ -87,6 +87,20 @@ protected function ensureStorage() {
    +  protected function createHtaccess($directory) {
    

    Given we're going to remove this at some stage, should we make it private instead?

  3. +++ b/core/lib/Drupal/Core/File/HtaccessWriter.php
    @@ -0,0 +1,158 @@
    +      // @todo https://www.drupal.org/node/2696103 catch a more specific exception
    +      //   and simplify this code.
    

    nit - phps > 80 chars

  4. +++ b/core/lib/Drupal/Core/File/HtaccessWriter.php
    @@ -0,0 +1,158 @@
    +      $htaccessPath = file_stream_wrapper_uri_normalize($directory . '/.htaccess');
    

    Do we want this call to procedural code to be wrapped in a private method so we can extract easily later?

  5. +++ b/core/lib/Drupal/Core/File/HtaccessWriter.php
    @@ -0,0 +1,158 @@
    +      // in the latest security update.
    

    should we remove the 'latest security update' comment now, its not really correct anymore?

  6. +++ b/core/lib/Drupal/Core/File/HtaccessWriter.php
    @@ -0,0 +1,158 @@
    +   * Return the list of HtAccess files to scan.
    

    nit: this is the only place we capitalize Access. should it just be '.htacess files to scan'?

phenaproxima’s picture

Status: Needs work » Needs review
StatusFileSize
new26.94 KB
new3.89 KB

Took a crack at addressing @larowlan's feedback.

kim.pepper’s picture

Status: Needs review » Reviewed & tested by the community

All feedback in #59 has been addressed so back to rtbc

catch’s picture

+++ b/core/lib/Drupal/Core/File/HtaccessWriter.php
@@ -0,0 +1,172 @@
+        $staging = config_get_config_directory(CONFIG_SYNC_DIRECTORY);

Is there already an issue to move this out of bootstrap.inc?

Rest of the patch looks OK, this looks slightly odd as a service to me, but it's probably because it's only being called by other legacy procedural code atm.

alexpott’s picture

There's no issue to deal with the $config_directories global. Configuring this in settings.php and having it available as a global feels super unnecessary now. I think this is a legacy of having an active directory and the live config being stored there. I think we could move this to $settings now and use the Settings singleton. I've created #3018015: Move the $config_directories to $settings so it can be accessed via the Settings singleton

alexpott’s picture

I was wrong there was an issue to deal with $config_directories - #2980712: Define config directory in settings

alexpott’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new5.16 KB
new25.23 KB
+++ b/core/lib/Drupal/Core/File/HtaccessWriter.php
@@ -0,0 +1,172 @@
+    foreach ($this->getHtaccessFiles() as $htaccessFile => $info) {
...
+  protected function getHtaccessFiles() {

+++ b/core/lib/Drupal/Core/File/HtaccessWriterInterface.php
@@ -0,0 +1,42 @@
+  public function checkRequirements();

+++ b/core/modules/system/system.install
@@ -490,35 +490,10 @@ function system_requirements($phase) {
-    foreach ($htaccess_files as $htaccess_file => $info) {
-      // Check for the string which was added to the recommended .htaccess file
-      // in the latest security update.
-      if (!file_exists($htaccess_file) || !($contents = @file_get_contents($htaccess_file)) || strpos($contents, 'Drupal_Security_Do_Not_Remove_See_SA_2013_003') === FALSE) {
-        $url = 'https://www.drupal.org/SA-CORE-2013-003';
-        $requirements[$htaccess_file] = [
-          'title' => $info['title'],
-          'value' => t('Not fully protected'),
-          'severity' => REQUIREMENT_ERROR,
-          'description' => t('See <a href=":url">@url</a> for information about the recommended .htaccess file which should be added to the %directory directory to help protect against arbitrary code execution.', [':url' => $url, '@url' => $url, '%directory' => $info['directory']]),
-        ];
-      }
-    }

Looking at this now. I'm not sure that the requirements stuff should be here on the interface. In some future world where requirement gathering will be an event maybe this service will listen to that event can construct the appropriate Requirement objects to place text on the requirements page but we're a long way from that. We could consider making getHtaccessFiles() public and using that in system_requirements(). I realise that the existence of checkRequirements on the interface is my fault - cf #50. So here's a patch

kim.pepper’s picture

Status: Needs review » Reviewed & tested by the community

I think you're right re the requirements stuff. LGTM.

  • catch committed 56ccba0 on 8.7.x
    Issue #2620304 by kim.pepper, andypost, phenaproxima, alexpott: htaccess...
catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed 56ccba0 and pushed to 8.7.x. Thanks!

berdir’s picture

+++ b/core/includes/file.inc
@@ -560,7 +531,7 @@ function file_unmanaged_prepare($source, &$destination = NULL, $replace = FILE_E
   }
   // Make sure the .htaccess files are present.
-  file_ensure_htaccess();
+  \Drupal::service('file.htaccess_writer')->ensure();
   return TRUE;

+++ b/core/lib/Drupal/Core/File/HtaccessWriter.php
@@ -0,0 +1,143 @@
+   * {@inheritdoc}
+   */
+  public function save($directory, $private = TRUE, $forceOverwrite = FALSE) {
+    if ($this->fileSystem->uriScheme($directory)) {
+      $htaccessPath = $this->normalizeUri($directory . '/.htaccess');
+    }
+    else {

Sorry for being late to the party, but this introduces some tricky cross-dependency issues.

file_unmanaged_prepare() is being moved into the fileSystem service in #2244513: Move the unmanaged file APIs to the file_system service (file.inc). Which is injected into this new service. That means they will both need each other.

Similar with the config stuff, not sure what the plan is with config_get_config_directory(), but it's a bit strange that this low-level API is calling out to the config system, and then Config\FileStorage uses this API as well. Likely config_get_config_directory() will be even more low-level and possibly just a static function (maybe on Settings?), so possibly that's not a big issue.

catch’s picture

config_get_config_directory() is being dealt with here #2980712: Define config directory in settings.

file_unmanaged_prepare() is a bit trickier to deal with - only thing I can really think of right now is incorporating this into the fileSystem service?

catch’s picture

catch’s picture

Status: Fixed » Needs review

Reverted the patch so we can discuss #69 more.

  • catch committed ae7a94a on 8.7.x
    Revert "Issue #2620304 by kim.pepper, andypost, phenaproxima, alexpott:...
alexpott’s picture

One advantage of having this as a separate service is that nginx users (or other webserver users) can swap this out for different implementations. Which I think I think is a good thing. So maybe an event that'll allow us to break the circular dependency is a good idea (as per @Berdir in Slack).

berdir’s picture

Status: Needs review » Reviewed & tested by the community

If we agree that having an event in the file_system service is the best way forward, then I think we can commit this again and then introduce that over there? We can only do it after one of these issues is in anyway.

kim.pepper’s picture

Agree that an event is a good idea. :-)

catch’s picture

I don't think we should add events solely to break circular dependencies unless there's no other choice, there's still a circular dependency when we do that, just one that's obscured by a layer of indirection.

However alexpott reminded me about issues like #2619250: Make .htaccess usage work for the widest possible configurations without relaxing security and document pitfalls which provide a use-case for making things alterable here, so that seems a better reason to do it.

kim.pepper’s picture

So are we agreed this approach is the preferred one? Or should we postpone on #2244513: Move the unmanaged file APIs to the file_system service (file.inc)

joachim’s picture

Status: Reviewed & tested by the community » Needs work

This doesn't seem to handle the htaccess writing that's done in \Drupal\Component\PhpStorage::ensureDirectory().

+++ b/core/lib/Drupal/Core/File/HtaccessWriter.php
@@ -0,0 +1,143 @@
+<?php
+
+namespace Drupal\Core\File;
+
+use Drupal\Component\PhpStorage\FileStorage;
+use Drupal\Core\StreamWrapper\PrivateStream;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Provides functions to manage Apache .htaccess files.
+ */
+class HtaccessWriter implements HtaccessWriterInterface {

Also, because Drupal\Component\PhpStorage::ensureDirectory is in Drupal\Component, that wouldn't be able to use this service, which is in Drupal\Core.

andypost’s picture

@joachim surely we can't use services in component namespace, that's why it using file_ functions and duplicates code a bit, but nice catch... not sure we can do anything about it

joachim’s picture

> surely we can't use services in component namespace

Could we put this new htaccess service in the Component namespace, and then Component\PhpStorage can depend on it?

If there are aspects of the htaccess service that need to be in the Core namespace, we can add a 2nd htaccess service there that subclasses / replaces / wraps the Component namespace service.

kim.pepper’s picture

That might be an improvement, but I don't think it untangles the circular dependencies.

jibran’s picture

Status: Needs work » Needs review
StatusFileSize
new638 bytes
new25.04 KB

Here is a reroll of #65.

berdir’s picture

  1. +++ b/core/includes/file.inc
    @@ -354,31 +338,18 @@ function file_ensure_htaccess() {
     /**
    @@ -563,7 +534,7 @@ function file_unmanaged_prepare($source, &$destination = NULL, $replace = FILE_E
    
    @@ -563,7 +534,7 @@ function file_unmanaged_prepare($source, &$destination = NULL, $replace = FILE_E
         return FALSE;
       }
       // Make sure the .htaccess files are present.
    -  file_ensure_htaccess();
    +  \Drupal::service('file.htaccess_writer')->ensure();
       return TRUE;
     }
     
    

    this function is now deprecated, so we might want to not even touch it. However, what we do need to touch now is the one inside file_system that replaces this. In #3034072: Move file uri/scheme functions from file.inc and FileSystem to StreamWrapperManager I realized that prepare stuff is even more fun than I thought, because it uses file_build_uri(), which in turn uses \file_default_scheme(), which uses \Drupal::config().

    With the final patch for the unmanaged functions, that is now a protected function that is not called from the outside anymore, it's just for copy() and move().

    Still thinking how we can make that a service or so, but it seems a bit weird that the crucial validation of source and destination would be in a separate service. One option might be to deprecate the empty-destination thing to get rid of that at least?

    And then it's just this htaccess stuff, we could do a setter-injection of this service in file_system?

  2. +++ b/core/lib/Drupal/Core/Config/FileStorage.php
    @@ -87,6 +87,20 @@ protected function ensureStorage() {
    +  private function createHtaccess($directory) {
    +    // @todo Properly inject services https://www.drupal.org/node/2940135
    +    return \Drupal::service('file.htaccess_writer')->save($directory, TRUE, TRUE);
    +  }
    

    In that issue, I proposed to use a get/set pattern with optional injection for file_system, we could possibly do the same for this service here? Becaue right now, that other service is already RTBC, but we'll see if it gets in.

Status: Needs review » Needs work

The last submitted patch, 83: 2620304-83.patch, failed testing. View results

andypost’s picture

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

kostyashupenko’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
StatusFileSize
new25 KB

Status: Needs review » Needs work

The last submitted patch, 88: 2620304-88.patch, failed testing. View results

joachim’s picture

> That might be an improvement, but I don't think it untangles the circular dependencies.

Where do we get circularity?

I think that we should take a step back here, and pause work on the patch. We should research in the code all the places that need to write htaccess files, and then figure out the dependency hierarchy.

kim.pepper’s picture

Status: Needs work » Needs review
StatusFileSize
new2.5 KB
new26.72 KB

Re-roll of #88 adding the setter injection of htaccessWriter into FileSystem service as per @Berdir's suggestion in #84.

Re: #90 @joachim the circular dependency is: FileSystem -> HtaccessWriter -> FileSystem, hence the setter injection here.

Status: Needs review » Needs work

The last submitted patch, 91: 2620304-htaccess-91.patch, failed testing. View results

kim.pepper’s picture

Status: Needs work » Needs review
StatusFileSize
new2.21 KB
new26.21 KB

Looks like symfony is detecting a circular reference even with setter injection. Trying a lazy-loading approach instead.

berdir’s picture

Status: Needs review » Needs work
  1. +++ b/core/includes/file.inc
    @@ -319,30 +318,15 @@ function file_prepare_directory(&$directory, $options = FileSystemInterface::MOD
    +  @trigger_error("file_ensure_htaccess() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\File\HtaccessWriter::ensure() instead. See https://www.drupal.org/node/2940126", E_USER_DEPRECATED);
    

    8.8 now

  2. +++ b/core/includes/install.core.inc
    @@ -1113,14 +1113,11 @@ function install_base_system(&$install_state) {
    +  // directory and config directory) have appropriate .htaccess files. These
    +  // directories will have already been created by this point in the installer,
    +  // since Drupal creates them during the install_verify_requirements() task.
    +  \Drupal::service("file.htaccess_writer")->ensure();
    

    single quotes?

  3. +++ b/core/lib/Drupal/Core/Config/FileStorage.php
    @@ -87,6 +87,20 @@ protected function ensureStorage() {
    +  private function createHtaccess($directory) {
    +    // @todo Properly inject services https://www.drupal.org/node/2940135
    +    return \Drupal::service('file.htaccess_writer')->save($directory, TRUE, TRUE);
    +  }
    

    this issue was closed with the final approach being exactly this, so we can remove the todo, maybe comment why we do it like this.

  4. +++ b/core/lib/Drupal/Core/File/FileSystem.php
    @@ -516,8 +523,7 @@ protected function prepareDestination($source, &$destination, $replace) {
         // Make sure the .htaccess files are present.
    -    // @todo Replace with a service in https://www.drupal.org/project/drupal/issues/2620304.
    -    file_ensure_htaccess();
    +    $this->getHtaccessWriter()->ensure();
    

    Not sure what to do with this.

    I'm not sure why these two lines even exist. They have been like this since 2009, as part of the stream wrapper API introduction. (still inlined into copy back then)

    But it's just about ensuring the .htaccess files in the root-level public://, private:// and so on exist, why does a copy/move need to ensure that?

    Do we have any tests that fail if we remove it?

  5. +++ b/core/tests/Drupal/KernelTests/Core/File/DirectoryTest.php
    @@ -94,7 +94,7 @@ public function testFileCheckDirectoryHandling() {
         $this->assertFalse(is_file(file_default_scheme() . '://.htaccess'), 'Successfully removed the .htaccess file in the files directory.', 'File');
    -    file_ensure_htaccess();
    +    $this->container->get("file.htaccess_writer")->ensure();
         $this->assertTrue(is_file(file_default_scheme() . '://.htaccess'), 'Successfully re-created the .htaccess file in the files directory.', 'File');
    

    some more double quotes.

  6. +++ b/core/tests/Drupal/KernelTests/Core/File/HtaccessTest.php
    @@ -9,42 +9,63 @@
    +
    +  /**
    +   * The Htaccess class under test.
    +   *
    +   * @var \Drupal\Core\File\HtaccessWriterInterface
    +   */
    +  protected $htaccess;
    

    HtaccessWriter maybe, for variable and inline reference?

kim.pepper’s picture

Status: Needs work » Needs review
  1. Fixed
  2. Fixed
  3. Fixed
  4. I'll do this in a separate patch
  5. Fixed
  6. Renamed the variable, but IMO it makes it easier to read to keep it as a property and initialise it in the setUp()
kim.pepper’s picture

StatusFileSize
new9.46 KB
new26.33 KB

Oops forgot to add patches.

kim.pepper’s picture

StatusFileSize
new1.32 KB
new25.52 KB

Here's a patch for #94.4 removing the call to $this->getHtaccessWriter()->ensure() inside prepareDestination()

berdir’s picture

+++ b/core/lib/Drupal/Core/File/FileSystem.php
@@ -515,9 +515,6 @@ protected function prepareDestination($source, &$destination, $replace) {
       throw new FileException("File '$source' could not be copied because it would overwrite itself.");
     }
-    // Make sure the .htaccess files are present.
-    // @todo Replace with a service in https://www.drupal.org/project/drupal/issues/2620304.
-    file_ensure_htaccess();
   }

The big question is whether this is OK. I asked @alexpott and @dww about this, but they weren't sure either. I *think* it is, because they only care about the top-level directory and prepareDestination() will at most once in a lifetime of a drupal site attempt to actually create sites/default/files (not sure it tries to do that at all), so why does it need to check that every. single. time we copy/move a file around? Makes no sense to me :)

kim.pepper’s picture

So where do we go from here? I'm tagging Needs framework manager review to get some input from the framework managers themselves.

berdir’s picture

Status: Needs review » Reviewed & tested by the community

I guess that makes sense, and also back to RTBC, as I said, IMHO this removal makes sense :)

johnwebdev’s picture

This is a pretty good example where OOP shines. We're now able to swap this implementation for another that nullifies the behaviour, i.e. if you use nginx as a web server, there is no need for .htaccess files to be created.

Sweet :)

kim.pepper’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs reroll
kim.pepper’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
StatusFileSize
new25.52 KB
kim.pepper’s picture

Status: Needs review » Reviewed & tested by the community

Just a re-roll so back to RTBC

kim.pepper’s picture

StatusFileSize
new25.5 KB

Re-roll of #103

kim.pepper’s picture

StatusFileSize
new25.5 KB
new3.59 KB

Updated to use latest deprecation message format.

andypost’s picture

kim.pepper’s picture

StatusFileSize
new25.67 KB

Re-roll of #106. Also updated deprecation message format to comply with coding standards.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 108: 2620304-108.patch, failed testing. View results

kim.pepper’s picture

Status: Needs work » Needs review
StatusFileSize
new25.91 KB
new2.97 KB

Removes usages of deprecated FileSystem::uriScheme() and file_stream_wrapper_uri_normalize()

kim.pepper’s picture

Status: Needs review » Needs work
Issue tags: +Needs reroll
kim.pepper’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
StatusFileSize
new25.39 KB

Status: Needs review » Needs work

The last submitted patch, 112: 2620304-112.patch, failed testing. View results

kim.pepper’s picture

Status: Needs work » Needs review
StatusFileSize
new25.41 KB
new652 bytes

Status: Needs review » Needs work

The last submitted patch, 114: 2620304-114.patch, failed testing. View results

kim.pepper’s picture

Status: Needs work » Needs review
StatusFileSize
new25.41 KB
new672 bytes

Gah, not sure why file_save_htaccess($public) is false in the deprecation test?

berdir’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/Config/FileStorage.php
    @@ -79,7 +79,7 @@ protected function ensureStorage() {
         // Only create .htaccess file in root directory.
         if ($dir == $this->directory) {
    -      $success = $success && file_save_htaccess($this->directory, TRUE, TRUE);
    +      $success = $success && $this->createHtaccess($this->directory);
         }
         if (!$success) {
    
    +++ b/core/lib/Drupal/Core/File/HtaccessWriter.php
    @@ -0,0 +1,133 @@
    +
    +      $staging = Settings::get('config_sync_directory', FALSE);
    +      if ($staging) {
    +        // Note that we log an error here if we can't write the .htaccess file.
    +        // This can occur if the staging directory is read-only. If it is then
    +        // it is the user's responsibility to create the .htaccess file.
    +        $this->save($staging, TRUE);
    +      }
    

    I'm wondering if the call in FileStorage is even still necessary. We are explicitly checking the staging idrectory in the ensure() method and support for other config directories is deprecated per https://www.drupal.org/node/3018145.

    But maybe we can leave it like this and think about that in a follow-up? Now that it's a private method, we can always remove it later.

  2. +++ b/core/lib/Drupal/Core/File/FileSystem.php
    @@ -505,9 +505,6 @@ protected function prepareDestination($source, &$destination, $replace) {
           throw new FileException("File '$source' could not be copied because it would overwrite itself.");
         }
    -    // Make sure the .htaccess files are present.
    -    // @todo Replace with a service in https://www.drupal.org/project/drupal/issues/2620304.
    -    file_ensure_htaccess();
       }
    

    I pointed out before why I think it is OK to remove this call, see #98

  3. Gah, not sure why file_save_htaccess($public) is false in the deprecation test?

    Because the parent directory doesn't exist apparently, adding this line above makes it work:

    \Drupal::service('file_system')->prepareDirectory($public, FileSystemInterface::CREATE_DIRECTORY);

    Lets add that to make it a bit more realistic, then I'll set it back to RTBC.

kim.pepper’s picture

Status: Needs work » Needs review
Related issues: +#3067440: Remove call to htaccess->ensure() in config FileStorage
StatusFileSize
new25.55 KB
new954 bytes
  1. Created follow-up #3067440: Remove call to htaccess->ensure() in config FileStorage
  2. Agree. We haven't had a framework manager agree to this yet though.
  3. Fixed
berdir’s picture

Status: Needs review » Reviewed & tested by the community

2. Yup, this was meant as a pointer for said framework maintainer :)

Back to RTBC.

kim.pepper’s picture

StatusFileSize
new25.38 KB

Re-roll of #118

mile23’s picture

larowlan’s picture

Status: Reviewed & tested by the community » Needs review
  1. +++ b/core/lib/Drupal/Core/Config/FileStorage.php
    @@ -87,6 +87,23 @@ protected function ensureStorage() {
    +  private function createHtaccess($directory) {
    +    return \Drupal::service('file.htaccess_writer')->save($directory, TRUE, TRUE);
    

    so we didn't opt for the event? I can't see any discussion of why the event idea was dropped after comment #77

  2. +++ b/core/lib/Drupal/Core/File/FileSystem.php
    @@ -508,9 +508,6 @@ protected function prepareDestination($source, &$destination, $replace) {
    -    // Make sure the .htaccess files are present.
    -    // @todo Replace with a service in https://www.drupal.org/project/drupal/issues/2620304.
    -    file_ensure_htaccess();
    

    I'm also not sure we can do this.

    Yes, we setup this file on install, but in some cases we copy databases around or migrate sites from one host to another, I think in those cases, we should be still ensuring this file exists.

  3. +++ b/core/lib/Drupal/Core/File/HtaccessWriter.php
    @@ -0,0 +1,133 @@
    +    else {
    +      $this->logger->error("Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: <pre><code>@htaccess

    ", [
    + '%directory' => $directory,
    + '@htaccess' => $htaccessLines,
    + ]);

    we don't need this else, we return above.

  4. +++ b/core/modules/system/system.install
    @@ -498,23 +498,10 @@ function system_requirements($phase) {
    +    $htaccessWriter->ensure();
    

    hmm, although we do end up creating them again here... which will alert the admin if update.module is enabled - see update_page_top

    What if we put a call somewhere like drupal_flush_all_caches as well, then perhaps we could do away with it in prepareDirectory

  5. +++ b/core/modules/system/system.module
    +++ b/core/modules/system/system.module
    @@ -933,13 +933,15 @@ function system_check_directory($form_element, FormStateInterface $form_state) {
    

    wow this function 🤔 only used by locale module but defined in system.module and in RAM on every drupal page request.

    Worth a follow up to deprecate it/move it to an include/move it to locale.module?

kim.pepper’s picture

  1. Yeah, I agree that got lost in the confusion. I read that comment from @catch and it sounded like he was against the idea of an event. But re-reading he says he thinks would be an ok situation for it.

    What would that look like? Would we just have an 'DirectoryCreated' event and have the HtAccessWriter a subscriber? If users are using nginx + php-fpm could just remove the subscriber?
  2. @Berdir commented in #98
    The big question is whether this is OK. I asked @alexpott and @dww about this, but they weren't sure either. I *think* it is, because they only care about the top-level directory and prepareDestination() will at most once in a lifetime of a drupal site attempt to actually create sites/default/files (not sure it tries to do that at all), so why does it need to check that every. single. time we copy/move a file around? Makes no sense to me :)
  3. Patching coming...
  4. I don't quite follow what you're suggesting here. 🤔
  5. Created #3073684: [pp-1] Deprecate system_check_directory()
kim.pepper’s picture

StatusFileSize
new25.35 KB
new1.16 KB

Patch for #123.3

larowlan’s picture

I don't quite follow what you're suggesting here. 🤔

I'm saying that the ::ensure call also happens when hook_requirements fires.
Which seems to occur from \Drupal\system\Controller\SystemController::overview which means any admin overview page would also trigger this, so maybe we don't need it for prepareDirectory.

kim.pepper’s picture

kim.pepper’s picture

Confirmed with @larowlan in slack that he meant prepareDestination() in #125, so I think that addresses #122.2 & .4

Just need some consensus on the event design decision?

kim.pepper’s picture

StatusFileSize
new66.67 KB
new44.67 KB

Had a go at using an event in config/FileStorage.php and it introduced a lot of changes.

Putting it up here to see what blows up.

andypost’s picture

Related issues: +#2657548: [PP-1] Add a factory method to create FileStorage instances

affecting this constructors always reminds me about related #2657548: [PP-1] Add a factory method to create FileStorage instances

Status: Needs review » Needs work

The last submitted patch, 128: 2620304-128.patch, failed testing. View results

kim.pepper’s picture

Status: Needs work » Needs review
StatusFileSize
new67.06 KB
new2.23 KB

Fixing some test fails.

Status: Needs review » Needs work

The last submitted patch, 131: 2620304-131.patch, failed testing. View results

kim.pepper’s picture

Having trouble tracking down FileStorage::__construct() with a NULL $event_dispatcher. 🤔

mile23’s picture

So here's the test fail for DefaultConfigTest:

Remaining deprecation notices (2)

  2x: Calling FileStorage::__construct() without the $event_dispatcher argument is deprecated in drupal:8.8.0 and is required in drupal:9.0.0. See https://www.drupal.org/node/2940126
    2x in DefaultConfigTest::testDefaultConfig from Drupal\KernelTests\Core\Config

Here's DefaultConfigTest::testDefaultConfig():

  public function testDefaultConfig() {
    $typed_config = \Drupal::service('config.typed');
    // Create a configuration storage with access to default configuration in
    // every module, profile and theme.
    $default_config_storage = new TestInstallStorage();
[ .. ]

What's a TestInstallStorage? It's a subclass of InstallStorage which is a subclass of FileStorage.

So there's one. :-)

Same deal for TestViewsTest.

There's also ExtensionInstallStorage which suffers the same fate in a few different tests, like for instance ExtensionInstallStorageTest.

berdir’s picture

Ouch. Maybe we can explore the event subscriber idea in a follow-up? Adding 40kb to this patch to avoid a single \Drupal::service() is pretty wild. We've struggled with dependencies of FileStorage before in #2940135: Use file_system service to \Drupal\Core\Config\FileStorage And while that was even worse due to cross-dependency, part of the decision was that we'd need so many changes.

kim.pepper’s picture

Yep sure. I guess that's a question for the framework managers?

andypost’s picture

I'd say it's best time to summon cmi2 initiative here - config storage is pita to extend but it often required, meantime they manage https://www.drupal.org/node/2943918
It looks more like "super class" which needs split, file system also becomes more "smart" so probably htaccess writer could live at composer level...

larowlan’s picture

+++ b/core/lib/Drupal/Core/Config/BootstrapConfigStorageFactory.php
@@ -54,7 +54,7 @@ public static function getDatabaseStorage() {
+    return new FileStorage(config_get_config_directory(CONFIG_ACTIVE_DIRECTORY), StorageInterface::DEFAULT_COLLECTION, \Drupal::service('event_dispatcher'));

yeah, sorry about the side-track, but we're kind of playing whack-a-mole here, one singleton call swapped for another. I'll discuss with another framework manager, but I'm leaning towards this not being worth it, unless we have a solid use-case for someone else implementing an event listener.

kim.pepper’s picture

OK cool. I think to untangle the mess we first need to fix:

  1. #2838474: Remove dependency of "file_system" service on "logger"
  2. then #2657548: [PP-1] Add a factory method to create FileStorage instances

This should help manage FileStorage changes going forward allowing us to introduce events etc.

kim.pepper’s picture

Status: Needs work » Needs review
StatusFileSize
new25.35 KB

Since we've agreed we're not going down the event route just now, here's a re-roll of #124 to get us back on track.

berdir’s picture

Status: Needs review » Reviewed & tested by the community

Per #127 and #138, lets try this again with the previous approach. We have issues in #139 to improve DI and avoid circular dependencies, until then, I think going with the full DI approach would be extremely challenging.

larowlan’s picture

I have pinged the security team for a review of the removed call in every prepareDestination

mcdruid’s picture

In #98 Berdir asked if it's okay to remove the call to "ensure" from prepareDestination().

One argument for ensuring that the .htaccess files are in place in the main files directories (/ stream wrappers) regularly is that it's not uncommon for (e.g. object injection) exploits to be able to delete files.

If this happens and the .htaccess in the top of the public files directory is removed, an attacker may then be able to upload php files which the webserver would execute (in certain configurations).

Ensuring that the top-level .htaccess is in place before every upload mitigates this risk.

If that's removed, what else regularly ensures that the main files directories are protected? Per larowlan (in slack) it looks like with the patch from #140 the check is done on "a) status report b) install c) every admin/{system|config|help} etc page that uses the menu overview listing".

Perhaps that's sufficient in many cases, but it's not hard to imagine a site which would become more vulnerable as a result of this change.

If it's problematic (because of dependencies) to do the check from prepareDestination(), one option may be to add a check on cron runs? Not a perfect replacement, but it would offer a little extra protection.

mcdruid’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new26.04 KB
new775 bytes

Patch which adds the htaccess check to system_cron().

I did also consider adding it to file_cron(), but we run this check from system_requirements() at runtime, and not file_requirements().

You could possibly argue either way.

Let's see whether tests are okay with it added to system_cron().

If we want to keep this, we should add a test which removes a .htaccess file and then runs cron, to ensure it gets added back in again.

mcdruid’s picture

\Drupal\KernelTests\Core\File\DirectoryTest::testFileCheckDirectoryHandling already has this:

    // Remove .htaccess file to then test that it gets re-created.
    @$file_system->unlink($default_scheme . '://.htaccess');
    $this->assertFalse(is_file($default_scheme . '://.htaccess'), 'Successfully removed the .htaccess file in the files directory.', 'File');
    $this->container->get('file.htaccess_writer')->ensure();
    $this->assertTrue(is_file($default_scheme . '://.htaccess'), 'Successfully re-created the .htaccess file in the files directory.', 'File');

We could add another test which does the same thing but runs cron instead of calling ensure() directly.

I'm happy to work on that, but will wait for further (security) review before doing so.

mile23’s picture

Hey folks. Not to be a downer here or anything... But.. :-)

In #3057094-101: Add Composer vendor/ hardening plugin to core we need to be able to generate htaccess and web.config files to place within the vendor directory in a Composer plugin context (meaning: No Drupal core.) If we don't change anything about how drupal/core-php-storage works, then we're golden. However:

In #3013210-4: Add drupal/core-filesecurity component for writing htaccess files there's the thought of further teasing this htaccess behavior out into its own component.

The thing is, we need to do one or the other, sooner rather than later, so that we can progress on the Composer initiative. #3057094: Add Composer vendor/ hardening plugin to core is now blocked on #3013210: Add drupal/core-filesecurity component for writing htaccess files, which could also mean being blocked on this issue.

We're blocked because we want to build a Composer project template, and once that template out in the wild, it might well be tricky for users to update if our Composer plugin suddenly has different dependencies than the lock file they committed.

Personally I'm +1 on making another component that only knows how to make htaccess files in #3013210: Add drupal/core-filesecurity component for writing htaccess files, and decorating it as a service in this issue.

kim.pepper’s picture

Title: htaccess functions should be a service » [PP-1] htaccess functions should be a service
Status: Needs review » Postponed
Related issues: +#3013210: Add drupal/core-filesecurity component for writing htaccess files

Personally I'm +1 on making another component that only knows how to make htaccess files in #3013210: Add drupal/core-filesecurity component for writing htaccess files, and decorating it as a service in this issue.

Yeah I think that makes the most sense. There is so many dependencies in here that there's no way a replacement for file_ensure_htaccess() and file_save_htaccess() can be done with a component alone.

Postponing on #3013210: Add drupal/core-filesecurity component for writing htaccess files

kim.pepper’s picture

If/when #3013210: Add drupal/core-filesecurity component for writing htaccess files goes in I think we can replace lots of usages of file_save_htaccess() with just FileSecurity::writeHtaccess() component calls, rather than calling this new service. The component has zero dependencies, whereas this depends on StreamWrapperManager, FileSystem, and Logger adding a lot of complexity for such a low-level service.

The only differences now are:

  1. This code allows you to provide a stream wrapper
  2. rtrims trailing slashes
  3. Logs an error if the htaccess file couldn't be written

Let's do a review as part of this issue once the FileSecurity component lands.

kim.pepper’s picture

Title: [PP-1] htaccess functions should be a service » htaccess functions should be a service
Assigned: Unassigned » kim.pepper
Status: Postponed » Active
kim.pepper’s picture

Status: Active » Needs review
StatusFileSize
new25.27 KB
new25.27 KB

Here's a re-roll of #144.

I like the cron idea!

Status: Needs review » Needs work

The last submitted patch, 150: 2620304-150.patch, failed testing. View results

kim.pepper’s picture

StatusFileSize
new24.88 KB
new2.76 KB

Fixes test due to error message in dblog, trying to get the realpath() when calling \Drupal\Core\File\HtaccessWriter::ensure(). We don't need to use the realpath for file_put_contents().

This means we can remove file_system as a dependency of HtaccessWriter! 🎉

kim.pepper’s picture

Status: Needs work » Needs review
StatusFileSize
new24.86 KB
new994 bytes

Oops. Forgot to use $this->logger().

kim.pepper’s picture

So now HtaccessWriter::save() is just:

  1. Normalizing a stream wrapper or trimming trailing slashes from a directory
  2. Logging an error if it cannot be written.

I'm wondering how we provide guidance around when to use HtaccessWriter::save() over just FileSecurity::writeHtaccess() ?

andypost’s picture

@kim.pepper thanks! code looks much more clear now!

Btw looking through usage it looks there only 2 public methods needed for each directory ("ensure" + "getState") + internal "write" can catch real file system exceptions and use logger.
The static list (public, temp) ... [private] all could be declared in file_system as core defaults so writer can read it there.

+++ b/core/lib/Drupal/Core/File/HtaccessWriter.php
@@ -0,0 +1,111 @@
+    $htaccessFiles['temporary://.htaccess'] = [
+      'title' => new TranslatableMarkup('Temporary files directory'),
+      'directory' => 'temporary://',
+      'private' => TRUE,
+    ];

+++ b/core/lib/Drupal/Core/File/HtaccessWriterInterface.php
@@ -0,0 +1,44 @@
+   * @return array
+   *   An associative array of htaccess files keyed by path with following keys:
+   *   - title: The title of the location
+   *   - directory: The directory of the location
+   *   - private: Whether the .htaccess is placed in a directory considered to
+   *     be private.
+   */
+  public function getHtaccessFiles();

Better to convert array to valueObject... kinda fileSytem::getFileSecurity($dir...) then use its methods ensure() and status() (which is used a lot in system module)

kim.pepper’s picture

StatusFileSize
new30.05 KB
new20.44 KB

Thanks @andypost

I've moved HtaccessWriter::save() to a protected method (and re-named to write()). I've also updated the file_save_htaccess() deprecation message to refer people to FileSecurity. However, we still need to maintain BC so I've duplicated the streamwrapper normalize call and logging in the deprecated function.

I've also replaced usages in system_check_directory() to call FileSecurity::writeHtaccess() directly, as we a showing form errors if there is a problem.

I also liked the idea of a value object, so created \Drupal\Core\File\ProtectedDirectory and re-wrote getHtaccessFiles() to defaultProtectedDirs() that returns a ProtectedDirectory objects.

Let's see how this goes!

kim.pepper’s picture

I also liked the idea of a value object, so created \Drupal\Core\File\ProtectedDirectory and re-wrote getHtaccessFiles() to defaultProtectedDirs() that returns a ProtectedDirectory objects.

Forgot to mention I didn't move this to FileSystem, as I wanted to avoid having HtaccessWriter depend on FileSystem and re-introduce the circular dependency!

Status: Needs review » Needs work

The last submitted patch, 156: 2620304-156.patch, failed testing. View results

kim.pepper’s picture

Status: Needs work » Needs review
StatusFileSize
new30.29 KB
new930 bytes

Replace use of HtaccessWriter::save() with FileSecurity::writeHtaccess() in simpletest.install.

Status: Needs review » Needs work

The last submitted patch, 159: 2620304-159.patch, failed testing. View results

kim.pepper’s picture

Status: Needs work » Needs review
StatusFileSize
new30.7 KB
new3.09 KB

A couple of fails since htaccessWriter::write() was made protected. Changed it to public and @internal instead.

andypost’s picture

  1. +++ b/core/lib/Drupal/Core/Config/FileStorage.php
    @@ -79,7 +80,7 @@ protected function ensureStorage() {
    -      $success = $success && file_save_htaccess($this->directory, TRUE, TRUE);
    +      $success = $success && FileSecurity::writeHtaccess($this->directory);
    
    +++ b/core/lib/Drupal/Core/File/HtaccessWriter.php
    @@ -0,0 +1,114 @@
    +      $protected_dirs[] = new ProtectedDirectory('Private files directory', 'private://', TRUE);
    
    +++ b/core/lib/Drupal/Core/File/ProtectedDirectory.php
    @@ -0,0 +1,77 @@
    +  public function isPrivate() {
    +    return $this->private;
    
    +++ b/core/modules/system/system.install
    @@ -498,32 +499,20 @@ function system_requirements($phase) {
    +    $htaccessWriter->ensure();
    +    foreach ($htaccessWriter->defaultProtectedDirs() as $protected_dir) {
    +      $htaccess_file = $protected_dir->getPath() . '/.htaccess';
           // Check for the string which was added to the recommended .htaccess file
           // in the latest security update.
           if (!file_exists($htaccess_file) || !($contents = @file_get_contents($htaccess_file)) || strpos($contents, 'Drupal_Security_Do_Not_Remove_See_SA_2013_003') === FALSE) {
    
    +++ b/core/modules/system/tests/src/Functional/File/FileSaveHtaccessLoggingTest.php
    @@ -24,14 +24,16 @@ public function testHtaccessSave() {
    +    $this->assertFalse($htaccess->write($private, TRUE));
    

    This is only places where "private" used, but in most places directory is known when private
    I mean uri already has "private://"
    So second argument could be removed and only "overwrite" is a question

  2. +++ b/core/lib/Drupal/Core/File/HtaccessWriter.php
    @@ -0,0 +1,114 @@
    +    $protected_dirs = [];
    +    $protected_dirs[] = new ProtectedDirectory('Public files directory', 'public://');
    

    I bet first line not needed

kim.pepper’s picture

StatusFileSize
new30.67 KB
new666 bytes
  1. Hmm. That would require us to special-case private:// and remove the ability for HtaccessWriter::write() to handle other private directories?
  2. Removed.
imyaro’s picture

Still no progress?
Point #1 - agree with kim.pepper, we should leave an ability to mark custom directories outside the private:// as "Private"

andypost the rest is OK for you?

kim.pepper’s picture

@zvse feel free to review and mark rtbc if you feel it is ready!

jibran’s picture

Status: Needs review » Reviewed & tested by the community

I think the patch is ready now.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 163: 2620304-163.patch, failed testing. View results

kim.pepper’s picture

Status: Needs work » Reviewed & tested by the community

Testbot wooble

imyaro’s picture

Status: Reviewed & tested by the community » Needs work

From my side:
1. I don't think that defaultProtectedDirs is correct method name. it is not default directories, it is kind of list of directories
Probably "getProtectedDirs" will be better.
2. No interface for the ProtectedDirectory
I think we need it to determine which methods are required and which are not. It will help us to develop custom protected directories
3. We don't have a chance to add new ProtectedDirectory without overriding the file.htaccess_writer service. Probably it is OK, but I love flexibility

Changing to needs work mainly because of #2

kim.pepper’s picture

Status: Needs work » Reviewed & tested by the community
  1. I think it's ok. It's the list of default directories. :-)
  2. ProtectedDirectory is a value object and doesn't need an interface IMO.
  3. HtaccessWriter::ensure() is only meant to be used to protect known core directories of public, private, and temporary. Any core/contrib modules that want to protect other directories should use the \Drupal\Component\FileSecurity\FileSecurity::writeHtaccess(), as we are did in #3057094: Add Composer vendor/ hardening plugin to core

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 163: 2620304-163.patch, failed testing. View results

kim.pepper’s picture

Status: Needs work » Reviewed & tested by the community

More testbot glitches.

larowlan’s picture

Pinged the security team again about a final review and removing the needs security review tag

kim.pepper’s picture

Issue summary: View changes

Updated change record and issue summary to explain that file_save_htaccess() is deprecated in favour of \Drupal\Component\FileSecurity\FileSecurity::writeHtaccess().

mcdruid’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new31.67 KB
new1.67 KB

One more thing I'd add is a basic test to ensure that cron restores a missing .htaccess - I've added that (per #145).

If tests pass, I'll move it back to RTBC and remove the "needs security review" tag.

mcdruid’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs security review

:thumbsup:

larowlan’s picture

  • larowlan committed d8ec05c on 8.8.x
    Issue #2620304 by kim.pepper, andypost, phenaproxima, mcdruid, alexpott...
larowlan’s picture

Status: Reviewed & tested by the community » Fixed

Committed d8ec05c and pushed to 8.8.x. Thanks!

🎉 thanks everyone for hanging in there on this one!

published the change record

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.