Last updated 19 September 2017. Created on 17 July 2016.
Edited by klausi, pmagunia. Log in to edit this page.

Traditionally all Drupal core, vendor and module PHP files are in the webserver accessible document root folder. This is a security weakness because Remote Code Execution vulnerabilities are found all the time when random PHP files are directly accessible. For example Drupal 8.2.7 had to fix a vulnerability in the vendor directory where a library shipped a PHP file exploitable by directly accessing it.

It is possible to run a Drupal 8 site with a clean docroot folder that does not contain code files and is therefore a lot more secure by default.

Core patch for image styles

To make image style generation work apply the image style system patch.

Directory setup

Assuming you have a Drupal 8 folder like this:

drupal-8
- core
- index.php
- modules
- profiles
- sites
- themes
- vendor

Create a new folder "docroot" in your Drupal 8 folder, so it looks like this:

drupal-8
- core
- docroot
- index.php
- modules
- profiles
- sites
- themes
- vendor

docroot will be the new folder that the webserver points to.

Webserver config change

Point your webserver config to the new docroot subfolder.
Apache example:

<VirtualHost *:80>
        ServerAlias drupal-8.localhost
        DocumentRoot /home/klausi/workspace/drupal-8/docroot
        <Directory "/home/klausi/workspace/drupal-8/docroot">
                Options FollowSymLinks
                AllowOverride All
                Require all granted
        </Directory>
</VirtualHost>

Nginx example:

server {
        server_name drupal-8.localhost;
        root /home/klausi/workspace/drupal-8/docroot;
        # Other config options here ...
}

index.php and robots.txt in docroot

Create a small index.php file in the docroot folder that will just include the index.php file from Drupal core:

chdir('..');
require 'index.php';

Create links for robots.txt (and .htaccess if using Apache) in the docroot folder:

cd docroot
ln -s ../robots.txt
ln -s ../.htaccess

Public files directory

Create a folder for public files in docroot/files. It must be writeable by the webserver.

mkdir docroot/files

Edit your settings.php file (usually sites/default/settings.php) and set the following:

$settings['file_public_path'] = 'docroot/files';
// Replace http://drupal-8.localhost with your site's URL or $base_url if you have defined that.
$settings['file_public_base_url'] = 'http://drupal-8.localhost/files';

rsync script to mirror asset files

Since Drupal core and modules ship with JavaScript, CSS and image files we need to copy those to the webserver accessible docroot with a script like this placed in the old document root:

#!/bin/bash

SYNC_FOLDERS="core libraries modules profiles themes"

for DIR in $SYNC_FOLDERS; do
  rsync -mr --include='*.'{js,css,svg,png,jpg,jpeg,ico} --include='*/' --exclude='*' $DIR/ docroot/$DIR/
done

Execute this script every time you do a deployment. It can be part of your deployment script that runs git pull, composer install and drush updb for example.

Conclusion

Congratulations! You have now a much more secure Drupal 8 installation that has zero code files in the publicly accessible document root.

Looking for support? Visit the Drupal.org forums, or join #drupal-support in IRC.

Comments

Berdir’s picture

That's at least missing the statistics.php script from the statistics.module, so if you use that, you need to copy that as well.

therobyouknow’s picture

That's at least missing the statistics.php script from the statistics.module, so if you use that, you need to copy that as well.

Thanks berdir.

What in particular would cause statistics.php to be missed by the steps? (There are many other modules in core/modules directory path)

Fidelix’s picture

What he means is that statistics.php has to be accessible on the docroot.
Other contrib modules will have similar behavior as well.

I don't know if this method is worth the effort over a blacklist-all + whitelist few like Perusio does with Nginx.

therobyouknow’s picture

Thanks Fidelix

andypost’s picture

Elijah Lynn’s picture

This is a security weakness because it is not necessary to have code files in the docroot.

This should be elaborated upon. Why is it not necessary?

-----------------------------------------------
The Future is Open!

Fidelix’s picture

I guess the right question is "Why is it necessary?".

We can put the code files outside the docroot and not lose any functionality. That makes it not necessary.

michaellenahan’s picture

Somfai Tibor’s picture

#!/bin/bash

SYNC_FOLDERS="core libraries modules profiles themes"

for DIR in $SYNC_FOLDERS; do
  rsync -mr --include='*.'{js,css,svg,png,jpg,jpeg,ico} --include='*/' --exclude='*' $DIR/ docroot/$DIR/
done

The code would be more useful in php.
Such as php copy.

cd docroot
ln -s ../robots.txt
ln -s ../.htaccess

Many hosting providers prohibit the creation of the link.

Somfai Tibor’s picture

If the shell is forbidden

Use a copy.php v1.0:

<?php
function select_copy($src,$dst) {
    $allowedtypes=array("css"=>true,"js"=>true,"svg"=>true,"png"=>true,"jpg"=>true,"jpeg"=>true,"ico"=>true,"gif"=>true);
    $dir = opendir($src);
    @mkdir($dst);
    while(false !== ( $file = readdir($dir)) ) {
        if (( $file != '.' ) && ( $file != '..' )) {
            if ( is_dir($src . '/' . $file) ) {
		echo '<b>'.$dst . '/' . $file.'</b><br>';			
                select_copy($src . '/' . $file,$dst . '/' . $file);
            }
            else {
		$fileext=pathinfo($file, PATHINFO_EXTENSION);
		if( !empty($allowedtypes[strtolower($fileext)]) && $allowedtypes[strtolower($fileext)]===true){
                copy($src . '/' . $file,$dst . '/' . $file);
		echo $dst . '/' . $file.'<br>';
		}
            }
        }
    }
    closedir($dir);
}

select_copy('core/assets','docroot/core/assets');
select_copy('core/misc','docroot/core/misc');
select_copy('core/modules','docroot/core/modules');
select_copy('core/scripts','docroot/core/scripts');
select_copy('core/tests','docroot/core/tests');
select_copy('core/themes','docroot/core/themes');

select_copy('libraries','docroot/libraries'); //if use custom or contrib libraries
select_copy('modules','docroot/modules');     //if use custom or contrib modules
select_copy('profiles','docroot/profiles');   //if use custom or contrib profiles
select_copy('themes','docroot/themes');       //if use custom or contrib themes

This code copy all directory.

copy.php v2.0:

<?php
function select_copy($src,$dst) {
	$allowedtypes=array("css"=>true,"js"=>true,"svg"=>true,"png"=>true,"jpg"=>true,"jpeg"=>true,"ico"=>true,"gif"=>true);
    $dir = opendir($src);
	$dirmaker = 0;
    while(false !== ( $file = readdir($dir)) ) {
        if (( $file != '.' ) && ( $file != '..' )) {
            if ( is_dir($src . '/' . $file) ) {
                select_copy($src . '/' . $file,$dst . '/' . $file);
            }
            else {
		$fileext=pathinfo($file, PATHINFO_EXTENSION);
		if( !empty($allowedtypes[strtolower($fileext)]) && $allowedtypes[strtolower($fileext)]===true){
			if($dirmaker ==0){
			$patharray=explode('/', $dst);
			$path='';	
			foreach ($patharray as &$value) {
			    $path=$path.'/'.$value;
			    @mkdir(ltrim($path, '/'));
			}
			echo '<b>'.ltrim($path, '/').'</b><br>';
			$dirmaker =1;
			}
                copy($src . '/' . $file,$dst . '/' . $file);
				echo $dst . '/' . $file.'<br>';
				}
            }
        }
    }
    closedir($dir);
}

select_copy('core/assets','docroot/core/assets');
select_copy('core/misc','docroot/core/misc');
select_copy('core/modules','docroot/core/modules');
select_copy('core/scripts','docroot/core/scripts');
select_copy('core/tests','docroot/core/tests');
select_copy('core/themes','docroot/core/themes');

select_copy('libraries','docroot/libraries'); //if use custom or contrib libraries
select_copy('modules','docroot/modules');     //if use custom or contrib modules
select_copy('profiles','docroot/profiles');   //if use custom or contrib profiles
select_copy('themes','docroot/themes');       //if use custom or contrib themes

This code not copy empty directory