Secure Apache mass virtual-hosting with VirtualDocumentRoot and PHP open_basedir

Setting up VirtualHosts with a <VirtualHost> block in Apache's config file is fine for a small number of virtual hosts, but if you're going to be hosting a lot of sites (mass virtual hosting) then there are several drawbacks:

  • Config files will soon become unmanageable
  • Too many blocks can slow down Apache
  • Apache needs to be restarted every time you add/remove a virtual host

A good solution is to use the VirtualDocumentRoot directive provided by the mod_vhost_alias Apache module (you will probably need to recompile Apache with –enable-vhost-alias).

You can then configure a single VirtualHost like:

<VirtualHost *> 
Use CanonicalName off  # use the name from the Host: header instead of the DNS name
VirtualDocumentRoot /usr/local/apache/vhosts/%0/
php_admin_value open_basedir VIRTUAL_DOCUMENT_ROOT
<VirtualHost>

The above means that the domain name used to access the server will be used to create the DocumentRoot on the fly. So, a request for www.example.com would be served from /usr/local/apache/vhosts/www.example.com/.

The php_admin_value open_basedir VIRTUAL_DOCUMENT_ROOT line enables the open_basedir setting for PHP to limit scripts to opening files which are under their own document root - essential to stop PHP reading other files. Note that this does not prevent executing any program and reading files that way... it's still trivial to use system('cat /etc/passwd'); etc. To prevent that you'll want to use safe_mode or disable all execution functions.

To use the special value of VIRTUAL_DOCUMENT_ROOT for the PHP open_basedir setting you'll have to patch PHP to support it, applying the following patch to fopen_wrappers.c:

--- fopen_wrappers.c-orig       2006-07-21 13:40:22.000000000 +0100
+++ fopen_wrappers.c    2006-07-21 13:50:33.000000000 +0100
@@ -96,9 +96,23 @@
        char resolved_name[MAXPATHLEN];
        char resolved_basedir[MAXPATHLEN];
        char local_open_basedir[MAXPATHLEN];
+       char *local_open_basedir_sub; /* Substring pointer for strstr */
        int resolved_basedir_len;
        int resolved_name_len;
 
+       /* Special case for VIRTUAL_DOCUMENT_ROOT in the open_basedir
+       value, which gets changed to the document root (see:
+       http://www.macosx.com/newsgroups/archive/index.php/t-249090.html
+       -- DavidP */
+       if ((strcmp(PG(open_basedir), "VIRTUAL_DOCUMENT_ROOT") == 0) && SG(request_info).path_translated && *SG(request_info).path_translated ) {
+            
+            strlcpy(local_open_basedir, SG(request_info).path_translated, sizeof(local_open_basedir));
+            
+            local_open_basedir_sub=strstr(local_open_basedir,SG(request_info).request_uri);
+            /* Now insert null to break apart the string */
+            if (local_open_basedir_sub) *local_open_basedir_sub = '\0';
+        } else
+
        /* Special case basedir==".": Use script-directory */
        if (strcmp(basedir, ".") || !VCWD_GETCWD(local_open_basedir, MAXPATHLEN)
) {
                /* Else use the unmodified path */

Then obviously recompile + re-install PHP for it to take effect.

Note - I didn't write the patch above, I simply modified it slightly to apply it to my version of PHP, it's provided here for convenience.

Note - currently, this patch will only work if VIRTUAL_DOCUMENT_ROOT is the only entry for the open_basedir directive - it will not work if you try to specify more than one directory, like:

php_admin_value open_basedir VIRTUAL_DOCUMENT_ROOT:/tmp

The patch needs improving to just check whether VIRTUAL_DOCUMENT_ROOT is found anywhere in the open_basedir value, and, if so, replace it with the real value. If anyone wants to do this, I'd be very happy if you were willing to supply the patch and I'll host it here (with credit to you of course). If not, I may give it a crack myself - I'm no C programmer, but I'm sure I could give it a shot. If it would be of use to anyone, let me know (use the discussion facility at the bottom of this page).

If you want to disable all execution functions, use something like the following in php.ini:

disable_functions shell_exec,system,passthru,proc_open

Useful References:

Fixing $_SERVER['DOCUMENT_ROOT'] value for PHP

In my opinion, when VirtualDocumentRoot has been used, mod_vhost_alias should set the DOCUMENT_ROOT env var to the value it worked out. The Apache developers are not happy to implement this as it's changing the default behaviour of DOCUMENT_ROOT (or, “it's not a bug, it's a feature”).

I personally believe that mod_vhost_alias.c should do The Right Thing as far as the user is concerned, which would be to set the DOCUMENT_ROOT env var to whatever document root it figured out - that's the commonly-expected behaviour. It could always be added as an option, like the following patch does...

There's a discussion of this issue and a patch to mod_vhost_alias.c available on Apache's BugZilla system at: http:issues.apache.org/bugzilla/show_bug.cgi?id=26052. The patch's functionality is enabled by setting the directive SetVirtualDocumentRoot to “on” or “true” If you don't want to patch mod_vhost_alias.c, another simple “fix” for PHP scripts would be to use something like the following, either in an included 'header' file or something, or in a file which is automatically prepended to all PHP scripts using the php_auto_prepend_file setting: <code php> $_SERVER['DOCUMENT_ROOT'] = preg_replace('/(%0)/e', '$_SERVER[\'HTTP_HOST\']', $_SERVER['DOCUMENT_ROOT']); </code> So, you could bung the above in a file, then in the <VirtualHost> block above add php_admin_value auto_prepend_file /path/to/file.php ===== Seperate log files per host ===== In the above setup, there is no way to have seperate access logfiles per domain. That could be easily achieved by using a custom log format which includes the hostname, then writing a script which reads that format on stdin and splits into multiple output files based on the vhost used, and writing to the appropriate file. Apache would then be configured to log to that file as a pipe, with something like: <code apache|httpd.conf> CustomLog “| splitlogs.pl” </code> To achieve reasonble performance, the log splitting program could keep the various logfiles open, and select the appropriate filehandle to write to for each line it reads from stdin (possibly limiting the number kept open, favouring often-used ones... otherwise, if lots of different domains are used, it could run out of file descriptors... especially with a DoS attempt by making many requests with random Host: headers....) This solution would also need to take into account log rotation. Alternatively, allow Apache to log all requests to one big file, which is periodically rotated, with the log-splitting script processing the file and splitting it into the seperate files (or directly generating stats, or whatever is needed). A suitable log file format which includes the hostname is: <code apache|httpd.conf> # this log format can be split per-virtual-host based on the first field LogFormat ”%V %h %l %u %t \”%r\” %s %b” vcommon CustomLog logs/access_log vcommon </code> That is the common logfile format with the hostname prepended... the results will look like: <code> www.example.com 127.0.0.1 - - [01/Nov/2006:12:39:18 +0000] “GET /url/ HTTP/1.1” 200 5093 foo.example.net 127.0.0.1 - - [01/Nov/2006:12:40:04 +0000] “POST /admin.php HTTP/1.1” 200 33670 </code> ~~DISCUSSION~~

 
apache/securemassvhosting.txt · Last modified: 2010/02/26 10:45 (external edit)
 
Recent changes RSS feed Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki