Defindit Docs and Howto Home
title:Apache httpd suExec, Rewrite and VirtualHost
keywords:apache,suexec,su,exec,httpd,rewrite,permissions,privs,privileges,forbidden,uid,suid,script,document,root
description:How to get suExec working for Virtual Hosts and RewriteEngine examples for scripts.

Jan 03 2007

Table of contents
-----------------
Synopsis
What is DocumentRoot?
The workaround
Explanation
Test script
Debugging mod_rewrite and pattern matching




Synopsis
--------

Suexec works great if your files are in document root or in 
the userdir (assuming you have userdir enabled). However, to get
suexec to work with virtually hosted domains that aren't in the
standard document root or for URLs which don't contain ~userid, you
need the workaround the below. As far as I know, using the Apache
RewriteEngine as outlined below is secure. The Apache documentation
team could have saved us all a lot of work by including this fairly
trivial bit of code in their suexec page.

I'm running Apache 2, and as far as I know this also works with Apache
1.3.

The Apach 2 suexec docs are:
http://httpd.apache.org/docs-2.0/suexec.html

For the following examples, we'll assume that our machine has a
virtual host "example.com", and it is owned by a user "mst3k".



What is DocumentRoot?
---------------------

"document root" in this context is what is returned by suexec -V (you must be root to run
this command).

[root@example ~]# suexec -V
 -D AP_DOC_ROOT="/var/www"
 -D AP_GID_MIN=100
 -D AP_HTTPD_USER="apache"
 -D AP_LOG_EXEC="/var/log/httpd/suexec.log"
 -D AP_SAFE_PATH="/usr/local/bin:/usr/bin:/bin"
 -D AP_UID_MIN=500
 -D AP_USERDIR_SUFFIX="public_html"
[root@example ~]#   

There are several things we can conclude from this output above, and
some of these are none too obvious

- The VirtualHost directive SuexecUserGroup will only work for scripts
somewhere in AP_DOC_ROOT which is /var/www for this example. Changing
document root in a VirtualHost with the DocumentRoot directive will
*not* effect this setting. Directories symlinked as subdirectories of
/var/www are supported, and will suexec. 

- The userdir is public_html (as usual).

- Users with a uid under 500 can't suexec. Groups with gid under 100
  can't suexec.

There are some good practical reasons to locate every user's document
root in /home/user/public_html even when virtually hosting. It keeps
all the files owned by non-admin users in /home. The user's
public_html is a real directory in the user space, and not a symlink
to a subdirectory in /var/www. The /var file system doesn't need to be
large enough to accomodate web space (not a problem on most modern
systems, but a headache in the old days). If everything a user needs
is in /home/user, there is no need for symlinks to other parts of the
disk. Backups are easier when all the user data is in /home (I also
keep user's mail boxes in /home as well, i.e. /home/mst3k/Maildir and
the user's httpd logs are in /home/mst3k/logs).

The problem stems from how paranoid suexec is. If the URL contains
~userid, then suexec will happily su from apache to userid. However,
if the URL is a virtually hosted URL (in the identical directory) and
does not contain ~userid, then suexec will *not* su (switch user). For
example /home/mst3k/public_html is document root for the virtually
hosted example.com. Normally suexec will su for
http://example.com/~mst3k/test_id.pl, but will not su for
http://example.com/test_id.pl even though it is the same script in the
same directory.



The workaround
--------------

The following workaround applies to httpd.conf or .htaccess. However,
for it to work in .htaccess you'll need privileges. I've used
AllowOverride all, but some lesser privileges may work.

Copy the following lines into the highest level .htaccess file,
e.g. into /home/mst3k/public_html (as opposed to a sub directory of
public_html).

This version will only redirect Perl scripts (.pl). As far as I know,
it will work for scripts in subdirectories without the need for an
additional copy in each subdirectory's .htaccess file.

# Workaround to get non-tilde URLs to become
# tilde URLs so that Apache will correctly su to mst3k
# instead of running the scripts as user apache.

RewriteEngine on
RewriteBase /
RewriteCond %{REQUEST_URI} !~
RewriteCond %{REQUEST_URI} ^\/.*\.pl$
RewriteCond %{DOCUMENT_ROOT} \/home\/mst3k\/public_html
RewriteRule ^(.*)$ /~mst3k/$1 [L]



Below is the original version that I used. This had some "features":

1) It doesn't [L] and therefore the rule evaluation continues and
evaluation of following rules can lead to unexpected results.

2) Although this looks safe from infinite loops (redirection loops),
when working on an instance of this code, I got an infinite loop (it
may have involved a subdirectory, or could have been an unrelated
bug). The version above does not loop.

3) The use of the capturing regex to get the userid is clever and
flexible. It is also slightly less efficient than the hard coded
version.


RewriteEngine on
RewriteBase /
RewriteCond %{REQUEST_URI} !^/~.*$
RewriteCond %{DOCUMENT_ROOT} \/home\/(.*)\/public_html
RewriteRule ^(.*)$ /~%1/$1



Explanation
-----------



This is the explanation of the original version.

# Enable the rewrite engine
RewriteEngine on

# Set the path. I think / is equivalent to the current setting of DocumentRoot.
RewriteBase /

# RewriteCond statements must all be true for any following
#RewriteRule statements to run. All rules run, until a [L] (last) or a
#rule is false.

# REQUEST_URI must not contain a ~ i.e. must not be like http://example.com/~mst3k/
RewriteCond %{REQUEST_URI} !^/~.*$

# DOCUMENT_ROOT is matched against the regular expression
# /home/(.*)/public_html, and (.*) is captured in variable %1. 
# This captures the userid, in our examples mst3k. This is generalized
# to work without changes for the different users. You should not need
# to edit this code for different users. You could leave out this line
# and hard code the user id in the next line.
RewriteCond %{DOCUMENT_ROOT} \/home\/(.*)\/public_html

# The URL minus the domain name is matched by ^(.*)$, and the
# expression is captured in $1. Apache will internally rewrite the file
# found using %1 from above and $1. For example, if example.com is
# virtually hosted from directory /home/mst3k/ then
# http://example.com/foo/test.pl
# becomes /~mst3k/foo/test.pl
# This is the crux of the workaround: the rewritten URI contains a
# ~userid and therefore Apache will suexec.
RewriteRule ^(.*)$ /~%1/$1




Test script
-----------

You can test this with the following 4 line script. Save this as a
file such as /home/mst3k/public_html/test_id.pl. 

#!/usr/bin/perl
use strict;
my $id_info = `/usr/bin/id`;
print "Content-type: text/html\n\n<html><body>$id_info</body></html>\n";



Run a command to get the file permissions right:
chown +x,go-rw test_id.pl

I assume you are running CGI scripts, or there's little reason to need
suexec, but nonetheless httpd.conf or .htaccess needs:

AddHandler cgi-script .cgi .pl

Scripts won't run if any of the directories containing the script are
group writable.

When the rewrite works, these two URLs give identical results:

http://example.com/~mst3k/test_id.pl
http://example.com/test_id.pl

Simply, a web page like this:

uid=501(mst3k) gid=501(mst3k) groups=48(apache),501(mst3k) 


Without the rewrite (or if it isn't working), only the ~mst3k script
will be userid mst3k. Without suexec, all the userids/group ids will
be apache.




Debugging mod_rewrite and pattern matching
------------------------------------------

# Use something like this to debug pattern matching
#RewriteRule ^(.*)$ /~twl8n/index.html?$1 [R]