diff --git a/core/composer.json b/core/composer.json index 5d248a188e6a860988610f7de8b7de662227f812..f2440a73dec2186757263b1dd1e0f57437db96f3 100644 --- a/core/composer.json +++ b/core/composer.json @@ -85,6 +85,7 @@ "drupal/core-discovery": "self.version", "drupal/core-event-dispatcher": "self.version", "drupal/core-file-cache": "self.version", + "drupal/core-file-security": "self.version", "drupal/core-filesystem": "self.version", "drupal/core-gettext": "self.version", "drupal/core-graph": "self.version", diff --git a/core/includes/file.inc b/core/includes/file.inc index c246fe402f189a2571b58da0a777c28c124f25ee..edbd86c53faffef70b9e43353f034f618822f846 100644 --- a/core/includes/file.inc +++ b/core/includes/file.inc @@ -5,8 +5,8 @@ * API for handling file uploads and server file management. */ +use Drupal\Component\FileSecurity\FileSecurity; use Drupal\Component\FileSystem\FileSystem as ComponentFileSystem; -use Drupal\Component\PhpStorage\FileStorage; use Drupal\Component\Utility\Environment; use Drupal\Component\Utility\UrlHelper; use Drupal\Core\File\Exception\FileException; @@ -362,28 +362,18 @@ function file_save_htaccess($directory, $private = TRUE, $force_overwrite = FALS $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager'); if ($stream_wrapper_manager::getScheme($directory)) { - $htaccess_path = $stream_wrapper_manager->normalizeUri($directory . '/.htaccess'); + $directory = $stream_wrapper_manager->normalizeUri($directory); } else { $directory = rtrim($directory, '/\\'); - $htaccess_path = $directory . '/.htaccess'; } - if (file_exists($htaccess_path) && !$force_overwrite) { - // Short circuit if the .htaccess file already exists. + if (FileSecurity::writeHtaccess($directory, $private, $force_overwrite)) { return TRUE; } - $htaccess_lines = FileStorage::htaccessLines($private); - // Write the .htaccess file. - if (file_exists($directory) && is_writable($directory) && file_put_contents($htaccess_path, $htaccess_lines)) { - return \Drupal::service('file_system')->chmod($htaccess_path, 0444); - } - else { - $variables = ['%directory' => $directory, '@htaccess' => $htaccess_lines]; - \Drupal::logger('security')->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</code></pre>", $variables); - return FALSE; - } + \Drupal::logger('security')->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</code></pre>", ['%directory' => $directory, '@htaccess' => FileSecurity::htaccessLines($private)]); + return FALSE; } /** @@ -398,12 +388,12 @@ function file_save_htaccess($directory, $private = TRUE, $force_overwrite = FALS * The desired contents of the .htaccess file. * * @deprecated in Drupal 8.0.x-dev and will be removed before Drupal 9.0.0. - * Use \Drupal\Component\PhpStorage\FileStorage::htaccessLines(). + * Use \Drupal\Component\FileSecurity\FileSecurity::htaccessLines(). * * @see https://www.drupal.org/node/2418133 */ function file_htaccess_lines($private = TRUE) { - return FileStorage::htaccessLines($private); + return FileSecurity::htaccessLines($private); } /** diff --git a/core/lib/Drupal/Component/FileSecurity/FileSecurity.php b/core/lib/Drupal/Component/FileSecurity/FileSecurity.php new file mode 100644 index 0000000000000000000000000000000000000000..efe1119c2707aa01a0bb11e98042e796e79f0158 --- /dev/null +++ b/core/lib/Drupal/Component/FileSecurity/FileSecurity.php @@ -0,0 +1,160 @@ +<?php + +namespace Drupal\Component\FileSecurity; + +/** + * Provides file security functions. + */ +class FileSecurity { + + /** + * Writes an .htaccess file in the given directory, if it doesn't exist. + * + * @param string $directory + * The directory. + * @param bool $deny_public_access + * (optional) Set to FALSE to ensure an .htaccess file for an open and + * public directory. Default is TRUE. + * @param bool $force + * (optional) Set to TRUE to force overwrite an existing file. + * + * @return bool + * TRUE if the file already exists or was created. FALSE otherwise. + */ + public static function writeHtaccess($directory, $deny_public_access = TRUE, $force = FALSE) { + return self::writeFile($directory, '/.htaccess', self::htaccessLines($deny_public_access), $force); + } + + /** + * Returns the standard .htaccess lines that Drupal writes. + * + * @param bool $deny_public_access + * (optional) Set to FALSE to return the .htaccess lines for an open and + * public directory that allows Apache to serve files, but not execute code. + * The default is TRUE, which returns the .htaccess lines for a private and + * protected directory that Apache will deny all access to. + * + * @return string + * The desired contents of the .htaccess file. + * + * @see file_save_htaccess() + */ + public static function htaccessLines($deny_public_access = TRUE) { + $lines = static::htaccessPreventExecution(); + + if ($deny_public_access) { + $lines = static::denyPublicAccess() . "\n\n$lines"; + } + + return $lines; + } + + /** + * Returns htaccess directives to deny execution in a given directory. + * + * @return string + * Apache htaccess directives to prevent execution of files in a location. + */ + protected static function htaccessPreventExecution() { + return <<<EOF +# Turn off all options we don't need. +Options -Indexes -ExecCGI -Includes -MultiViews + +# Set the catch-all handler to prevent scripts from being executed. +SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006 +<Files *> + # Override the handler again if we're run later in the evaluation list. + SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003 +</Files> + +# If we know how to do it safely, disable the PHP engine entirely. +<IfModule mod_php5.c> + php_flag engine off +</IfModule> +<IfModule mod_php7.c> + php_flag engine off +</IfModule> +EOF; + } + + /** + * Returns htaccess directives to block all access to a given directory. + * + * @return string + * Apache htaccess directives to block access to a location. + */ + protected static function denyPublicAccess() { + return <<<EOF +# Deny all requests from Apache 2.4+. +<IfModule mod_authz_core.c> + Require all denied +</IfModule> + +# Deny all requests from Apache 2.0-2.2. +<IfModule !mod_authz_core.c> + Deny from all +</IfModule> +EOF; + } + + /** + * Writes a web.config file in the given directory, if it doesn't exist. + * + * @param string $directory + * The directory. + * @param bool $force + * (optional) Set to TRUE to force overwrite an existing file. + * + * @return bool + * TRUE if the file already exists or was created. FALSE otherwise. + */ + public static function writeWebConfig($directory, $force = FALSE) { + return self::writeFile($directory, '/web.config', self::webConfigLines(), $force); + } + + /** + * Returns the standard web.config lines for security. + * + * @return string + * The contents of the web.config file. + */ + public static function webConfigLines() { + return <<<EOT +<configuration> + <system.webServer> + <authorization> + <deny users="*"> + </authorization> + </system.webServer> +</configuration> +EOT; + } + + /** + * Writes the contents to the file in the given directory. + * + * @param string $directory + * The directory to write to. + * @param string $filename + * The file name. + * @param string $contents + * The file contents. + * @param bool $force + * TRUE if we should force the write over an existing file. + * + * @return bool + * TRUE if writing the file was successful. + */ + protected static function writeFile($directory, $filename, $contents, $force) { + $file_path = $directory . DIRECTORY_SEPARATOR . $filename; + // Don't overwrite if the file exists unless forced. + if (file_exists($file_path) && !$force) { + return TRUE; + } + if (file_exists($directory) && is_writable($directory) && file_put_contents($file_path, $contents)) { + return @chmod($file_path, 0444); + } + return FALSE; + } + +} diff --git a/core/lib/Drupal/Component/FileSecurity/LICENSE.txt b/core/lib/Drupal/Component/FileSecurity/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..94fb84639c4b6ff359e47a124d8fb4a3aba7a386 --- /dev/null +++ b/core/lib/Drupal/Component/FileSecurity/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/core/lib/Drupal/Component/FileSecurity/README.txt b/core/lib/Drupal/Component/FileSecurity/README.txt new file mode 100644 index 0000000000000000000000000000000000000000..a63fc780f8d75c74fa67b90de3bf4b92dd35ff0d --- /dev/null +++ b/core/lib/Drupal/Component/FileSecurity/README.txt @@ -0,0 +1,12 @@ +The Drupal FileSecurity Component + +Thanks for using this Drupal component. + +You can participate in its development on Drupal.org, through our issue system: +https://www.drupal.org/project/issues/drupal + +You can get the full Drupal repo here: +https://www.drupal.org/project/drupal/git-instructions + +You can browse the full Drupal repo here: +http://cgit.drupalcode.org/drupal diff --git a/core/lib/Drupal/Component/FileSecurity/TESTING.txt b/core/lib/Drupal/Component/FileSecurity/TESTING.txt new file mode 100644 index 0000000000000000000000000000000000000000..3def769ff5d0d6e5d63f5c308c428a32c8136c5d --- /dev/null +++ b/core/lib/Drupal/Component/FileSecurity/TESTING.txt @@ -0,0 +1,18 @@ +HOW-TO: Test this Drupal component + +In order to test this component, you'll need to get the entire Drupal repo and +run the tests there. + +You'll find the tests under core/tests/Drupal/Tests/Component. + +You can get the full Drupal repo here: +https://www.drupal.org/project/drupal/git-instructions + +You can find more information about running PHPUnit tests with Drupal here: +https://www.drupal.org/node/2116263 + +Each component in the Drupal\Component namespace has its own annotated test +group. You can use this group to run only the tests for this component. Like +this: + +$ ./vendor/bin/phpunit -c core --group FileSecurity diff --git a/core/lib/Drupal/Component/FileSecurity/composer.json b/core/lib/Drupal/Component/FileSecurity/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..039431346895ba2c1e085991f5a232b87404037c --- /dev/null +++ b/core/lib/Drupal/Component/FileSecurity/composer.json @@ -0,0 +1,15 @@ +{ + "name": "drupal/core-file-security", + "description": "FileSecurity.", + "keywords": ["drupal"], + "homepage": "https://www.drupal.org/project/drupal", + "license": "GPL-2.0-or-later", + "require": { + "php": ">=7.0.8" + }, + "autoload": { + "psr-4": { + "Drupal\\Component\\FileSecurity\\": "" + } + } +} diff --git a/core/lib/Drupal/Component/PhpStorage/FileStorage.php b/core/lib/Drupal/Component/PhpStorage/FileStorage.php index d00d2779e523117765b0e1692f44c67b272a3008..0486eb356964f5738ec5910a4b468ee58290b233 100644 --- a/core/lib/Drupal/Component/PhpStorage/FileStorage.php +++ b/core/lib/Drupal/Component/PhpStorage/FileStorage.php @@ -2,6 +2,8 @@ namespace Drupal\Component\PhpStorage; +use Drupal\Component\FileSecurity\FileSecurity; + /** * Stores the code as regular PHP files. */ @@ -64,46 +66,15 @@ public function save($name, $code) { * @return string * The desired contents of the .htaccess file. * - * @see file_create_htaccess() + * @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Instead use + * \Drupal\Component\FileSecurity\FileSecurity. + * + * @see https://www.drupal.org/node/3075098 + * @see file_save_htaccess() */ public static function htaccessLines($private = TRUE) { - $lines = <<<EOF -# Turn off all options we don't need. -Options -Indexes -ExecCGI -Includes -MultiViews - -# Set the catch-all handler to prevent scripts from being executed. -SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006 -<Files *> - # Override the handler again if we're run later in the evaluation list. - SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003 -</Files> - -# If we know how to do it safely, disable the PHP engine entirely. -<IfModule mod_php5.c> - php_flag engine off -</IfModule> -<IfModule mod_php7.c> - php_flag engine off -</IfModule> -EOF; - - if ($private) { - $lines = <<<EOF -# Deny all requests from Apache 2.4+. -<IfModule mod_authz_core.c> - Require all denied -</IfModule> - -# Deny all requests from Apache 2.0-2.2. -<IfModule !mod_authz_core.c> - Deny from all -</IfModule> - -$lines -EOF; - } - - return $lines; + @trigger_error("htaccessLines() is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Component\FileSecurity\FileSecurity::htaccessLines() instead. See https://www.drupal.org/node/3075098", E_USER_DEPRECATED); + return FileSecurity::htaccessLines($private); } /** @@ -122,10 +93,7 @@ public static function htaccessLines($private = TRUE) { */ protected function ensureDirectory($directory, $mode = 0777) { if ($this->createDirectory($directory, $mode)) { - $htaccess_path = $directory . '/.htaccess'; - if (!file_exists($htaccess_path) && file_put_contents($htaccess_path, static::htaccessLines())) { - @chmod($htaccess_path, 0444); - } + FileSecurity::writeHtaccess($directory); } } diff --git a/core/lib/Drupal/Component/PhpStorage/composer.json b/core/lib/Drupal/Component/PhpStorage/composer.json index e4ff9b69be2f440e333cf5f484b431c968d79838..e2ed734ca4a4c98e2952c0e5d24447523158db3c 100644 --- a/core/lib/Drupal/Component/PhpStorage/composer.json +++ b/core/lib/Drupal/Component/PhpStorage/composer.json @@ -5,7 +5,8 @@ "homepage": "https://www.drupal.org/project/drupal", "license": "GPL-2.0-or-later", "require": { - "php": ">=7.0.8" + "php": ">=7.0.8", + "drupal/core-file-security": "^8.8" }, "autoload": { "psr-4": { diff --git a/core/lib/Drupal/Core/Composer/Composer.php b/core/lib/Drupal/Core/Composer/Composer.php index 4119089a66a8821dec2069fb9f7ded845344a6ab..8a3bd28d3f5f86a51f6c2329fbc5f8f9537a27e9 100644 --- a/core/lib/Drupal/Core/Composer/Composer.php +++ b/core/lib/Drupal/Core/Composer/Composer.php @@ -2,13 +2,13 @@ namespace Drupal\Core\Composer; -use Drupal\Component\PhpStorage\FileStorage; -use Composer\Semver\Comparator; use Composer\Composer as ComposerApp; -use Composer\Script\Event; use Composer\Installer\PackageEvent; +use Composer\Script\Event; +use Composer\Semver\Comparator; use Composer\Semver\Constraint\Constraint; use Composer\Util\ProcessExecutor; +use Drupal\Component\FileSecurity\FileSecurity; /** * Provides static functions for composer script events. @@ -162,25 +162,10 @@ public static function ensureHtaccess(Event $event) { $vendor_dir = $event->getComposer()->getConfig()->get('vendor-dir'); // Prevent access to vendor directory on Apache servers. - $htaccess_file = $vendor_dir . '/.htaccess'; - if (!file_exists($htaccess_file)) { - file_put_contents($htaccess_file, FileStorage::htaccessLines(TRUE) . "\n"); - } + FileSecurity::writeHtaccess($vendor_dir); // Prevent access to vendor directory on IIS servers. - $webconfig_file = $vendor_dir . '/web.config'; - if (!file_exists($webconfig_file)) { - $lines = <<<EOT -<configuration> - <system.webServer> - <authorization> - <deny users="*"> - </authorization> - </system.webServer> -</configuration> -EOT; - file_put_contents($webconfig_file, $lines . "\n"); - } + FileSecurity::writeWebConfig($vendor_dir); } /** diff --git a/core/modules/system/tests/src/Functional/File/FileSaveHtaccessLoggingTest.php b/core/modules/system/tests/src/Functional/File/FileSaveHtaccessLoggingTest.php index 003373de718e5f310ae40126e1febf04669265a3..a75952d9088a883bc0a67d6ecaadbecc0154c934 100644 --- a/core/modules/system/tests/src/Functional/File/FileSaveHtaccessLoggingTest.php +++ b/core/modules/system/tests/src/Functional/File/FileSaveHtaccessLoggingTest.php @@ -2,7 +2,7 @@ namespace Drupal\Tests\system\Functional\File; -use Drupal\Component\PhpStorage\FileStorage; +use Drupal\Component\FileSecurity\FileSecurity; use Drupal\Tests\BrowserTestBase; /** @@ -29,7 +29,7 @@ public function testHtaccessSave() { $this->drupalGet('admin/reports/dblog'); $this->clickLink("Security warning: Couldn't write .htaccess file. Please…"); - $lines = FileStorage::htaccessLines(TRUE); + $lines = FileSecurity::htaccessLines(TRUE); foreach (array_filter(explode("\n", $lines)) as $line) { $this->assertEscaped($line); } diff --git a/core/tests/Drupal/KernelTests/Core/File/DirectoryTest.php b/core/tests/Drupal/KernelTests/Core/File/DirectoryTest.php index 796e87b4d6d3e0832ffa4bdfcbeb6b05fbada1e3..c8834e7bd8d50696cec81781543038aacf419a71 100644 --- a/core/tests/Drupal/KernelTests/Core/File/DirectoryTest.php +++ b/core/tests/Drupal/KernelTests/Core/File/DirectoryTest.php @@ -2,9 +2,9 @@ namespace Drupal\KernelTests\Core\File; +use Drupal\Component\FileSecurity\FileSecurity; use Drupal\Component\FileSystem\FileSystem; use Drupal\Component\Render\FormattableMarkup; -use Drupal\Component\PhpStorage\FileStorage; use Drupal\Core\File\Exception\FileException; use Drupal\Core\File\FileSystemInterface; @@ -101,7 +101,7 @@ public function testFileCheckDirectoryHandling() { $this->assertTrue(is_file($default_scheme . '://.htaccess'), 'Successfully re-created the .htaccess file in the files directory.', 'File'); // Verify contents of .htaccess file. $file = file_get_contents($default_scheme . '://.htaccess'); - $this->assertEqual($file, FileStorage::htaccessLines(FALSE), 'The .htaccess file contains the proper content.', 'File'); + $this->assertEqual($file, FileSecurity::htaccessLines(FALSE), 'The .htaccess file contains the proper content.', 'File'); } /** diff --git a/core/tests/Drupal/Tests/Component/FileSecurity/FileSecurityTest.php b/core/tests/Drupal/Tests/Component/FileSecurity/FileSecurityTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0066f53d7cf01181862070e558cc63a1f8bcf921 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/FileSecurity/FileSecurityTest.php @@ -0,0 +1,96 @@ +<?php + +namespace Drupal\Tests\Component\FileSecurity; + +use Drupal\Component\FileSecurity\FileSecurity; +use org\bovigo\vfs\vfsStream; +use PHPUnit\Framework\TestCase; + +/** + * Tests the file security component. + * + * @coversDefaultClass \Drupal\Component\FileSecurity\FileSecurity + * @group FileSecurity + */ +class FileSecurityTest extends TestCase { + + /** + * @covers ::writeHtaccess + */ + public function testWriteHtaccessPrivate() { + vfsStream::setup('root'); + FileSecurity::writeHtaccess(vfsStream::url('root')); + $htaccess_file = vfsStream::url('root') . '/.htaccess'; + $this->assertFileExists($htaccess_file); + $this->assertEquals('0444', substr(sprintf('%o', fileperms($htaccess_file)), -4)); + $htaccess_contents = file_get_contents($htaccess_file); + $this->assertContains("Require all denied", $htaccess_contents); + } + + /** + * @covers ::writeHtaccess + */ + public function testWriteHtaccessPublic() { + vfsStream::setup('root'); + $this->assertTrue(FileSecurity::writeHtaccess(vfsStream::url('root'), FALSE)); + $htaccess_file = vfsStream::url('root') . '/.htaccess'; + $this->assertFileExists($htaccess_file); + $this->assertEquals('0444', substr(sprintf('%o', fileperms($htaccess_file)), -4)); + $htaccess_contents = file_get_contents($htaccess_file); + $this->assertNotContains("Require all denied", $htaccess_contents); + } + + /** + * @covers ::writeHtaccess + */ + public function testWriteHtaccessForceOverwrite() { + vfsStream::setup('root'); + $htaccess_file = vfsStream::url('root') . '/.htaccess'; + file_put_contents($htaccess_file, "foo"); + $this->assertTrue(FileSecurity::writeHtaccess(vfsStream::url('root'), TRUE, TRUE)); + $htaccess_contents = file_get_contents($htaccess_file); + $this->assertContains("Require all denied", $htaccess_contents); + $this->assertNotContains("foo", $htaccess_contents); + } + + /** + * @covers ::writeHtaccess + */ + public function testWriteHtaccessFailure() { + vfsStream::setup('root'); + $this->assertFalse(FileSecurity::writeHtaccess(vfsStream::url('root') . '/foo')); + } + + /** + * @covers ::writeWebConfig + */ + public function testWriteWebConfig() { + vfsStream::setup('root'); + $this->assertTrue(FileSecurity::writeWebConfig(vfsStream::url('root'))); + $web_config_file = vfsStream::url('root') . '/web.config'; + $this->assertFileExists($web_config_file); + $this->assertEquals('0444', substr(sprintf('%o', fileperms($web_config_file)), -4)); + } + + /** + * @covers ::writeWebConfig + */ + public function testWriteWebConfigForceOverwrite() { + vfsStream::setup('root'); + $web_config_file = vfsStream::url('root') . '/web.config'; + file_put_contents($web_config_file, "foo"); + $this->assertTrue(FileSecurity::writeWebConfig(vfsStream::url('root'), TRUE)); + $this->assertFileExists($web_config_file); + $this->assertEquals('0444', substr(sprintf('%o', fileperms($web_config_file)), -4)); + $this->assertNotContains("foo", $web_config_file); + } + + /** + * @covers ::writeWebConfig + */ + public function testWriteWebConfigFailure() { + vfsStream::setup('root'); + $this->assertFalse(FileSecurity::writeWebConfig(vfsStream::url('root') . '/foo')); + } + +} diff --git a/core/tests/Drupal/Tests/Component/PhpStorage/FileStorageDeprecationTest.php b/core/tests/Drupal/Tests/Component/PhpStorage/FileStorageDeprecationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..7ca8c51955074399227fccf263427971736143e1 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/PhpStorage/FileStorageDeprecationTest.php @@ -0,0 +1,25 @@ +<?php + +namespace Drupal\Tests\Component\PhpStorage; + +use Drupal\Component\PhpStorage\FileStorage; +use PHPUnit\Framework\TestCase; + +/** + * Tests FileStorage deprecations. + * + * @coversDefaultClass \Drupal\Component\PhpStorage\FileStorage + * @group legacy + * @group Drupal + * @group PhpStorage + */ +class FileStorageDeprecationTest extends TestCase { + + /** + * @expectedDeprecation htaccessLines() is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Component\FileSecurity\FileSecurity::htaccessLines() instead. See https://www.drupal.org/node/3075098 + */ + public function testHtAccessLines() { + $this->assertNotEmpty(FileStorage::htaccessLines()); + } + +} diff --git a/core/tests/Drupal/Tests/Component/PhpStorage/MTimeProtectedFileStorageBase.php b/core/tests/Drupal/Tests/Component/PhpStorage/MTimeProtectedFileStorageBase.php index 7c4b787c34234eb41f3304cf21cf63792f5ff92f..ad78951713d4b522d84d9d314dd641e77d00f0d0 100644 --- a/core/tests/Drupal/Tests/Component/PhpStorage/MTimeProtectedFileStorageBase.php +++ b/core/tests/Drupal/Tests/Component/PhpStorage/MTimeProtectedFileStorageBase.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\Component\PhpStorage; +use Drupal\Component\FileSecurity\FileSecurity; use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\Random; @@ -96,7 +97,7 @@ public function testSecurity() { // Ensure the root directory for the bin has a .htaccess file denying web // access. - $this->assertSame(file_get_contents($expected_root_directory . '/.htaccess'), call_user_func([$this->storageClass, 'htaccessLines'])); + $this->assertSame(file_get_contents($expected_root_directory . '/.htaccess'), FileSecurity::htaccessLines()); // Ensure that if the file is replaced with an untrusted one (due to another // script's file upload vulnerability), it does not get loaded. Since mtime