Securing Apache Public Directories

Many sites on the web have use for a public directory where users can upload arbitrary files. There are numerous such cases -- avatars on web forums, patches for bug trackers, new media files for a wiki, and so on.

Naive implementations of such a feature pose a substantial security risk. Many common Apache configurations will interpret server-side any file containing a particular extension as a PHP script. This occurs for two reasons. One, the behavior of the Apache module mod_php, which implements the PHP interpreter as part of Apache (rather than externally). Two, the Apache main configuration (usually httpd.conf) or a satellite htaccess override has set one or more extensions to automatically be processed by mod_php. Importantly, this execution occurs on a simple HTTP GET request for the file, and does not require executable permission on the file.

In effect, this means if you provide public write access to a web-visible directory on any server with Apache set up this way, you are immediately vulnerable to remote script execution. The script will run with the same permissions Apache itself has. Fortunately, this is usually an isolated user such as "nobody", "apache", "httpd", or a similar limited access account. That's sufficient to prevent malicious scripts from wreaking utter havoc, and is probably the main reason why many PHP using sites haven't been trivially compromised.

Unfortunately, there are still many things restricted accounts can typically do on the server. Among them are spawning new processes, receiving and transmitting data across the network, reading many files, creating files in world-writable directories, and writing to any world-writable file. These simple functions are enough for hostile activity such as spamming or probing the server for privilege escalation exploits.

In very poor configurations, the owner of the web root is set as the same user that Apache is running as. In that case, the script has the capacity to reshape the site as it sees fit. (A clever script would not reveal itself by obvious defacement and data corruption, but rather silently install back doors like privileged user accounts.)

There are six plausible approaches to dealing with this issue without sacrificing the ability for arbitrary file upload:

  1. Move the upload directory outside the web root.
  2. Shut down all Apache scripting modules that allows execution without explicit filesystem executable permission, and run scripts in CGI mode.
  3. Quota the relevant Apache user into a tiny box from which little activity can escape.
  4. Rely on a central library or other specialized software to screen all uploads for malicious content.
  5. Lock down the public directories so that scripts cannot execute there.

As a precursor to the discussion of solutions, do note that everything below assumes that users don't have direct filesystem access. If the site's users can manipulate the filesystem via FTP, SSH, or other services, those need to be secured carefully first. Furthermore, double check the permissions of all relevant users and directories, user quotas, and program-specific security settings. Permissive defaults have caused havoc in the past.

When feasible, one of the best solutions is simply to maintain upload or other user-controlled storage directories in a place outside the web root. If the folders in question are not managed by Apache, they will also not be parsed for script content. Indeed, without some other form of access to the server, the files outside the tree of the web root are essentially invisible to the general public. There are a couple of tiny disadvantages to this approach. One, it may be necessary to update code or configuration files of some components. Two, it's difficult to implement a public script testing system or other kinds of experimental sandboxes with it.

Presuming that you have full access to the software environment and are willing to accept the performance loss, then another good option is to always use the common gateway interface (CGI) method of executing scripts. This allows you to use filesystem permissions for security; scripts not marked +x won't execute. Be sure to confirm that your CGI implementation actually behaves this way.

Setting sane quotas is a good idea in the broad view for other reasons, but it is not a proper solution to this matter. Setting CPU, memory, process, or bandwidth quotas too low will restrict your own ability to run useful scripts. Besides, attackers may find ways to accomplish their tasks even within the strict limits you set.

Examining all uploaded content sounds good until you try to establish any kind of principled theory on how it will work, or try to implement it in practice. There is no general way to distinguish malicious content from harmless content, and the reality is that false positives and false negatives will occur on a frequent basis. This will anger legitimate users and fail to thwart skilled cracking attempts.

Such a screening process is fundamentally equivalent to the principle behind anti-virus software. In short, create a heuristic algorithm which distinguishes good objects from bad objects based on their characteristics. This mechanism is an abject failure in practice; it combines the worst of two worlds with low accuracy and high resource use. Even more critically, it deals with the threat as it arrives rather than preventing the issue in the first place.

Lastly, we have disabling script execution only in the relevant public directories. It happens to be both easy to implement and effective, a rare combination. Apache, being one complicated beast, has a slew of different configuration options which are all related to this:

Each of these options has a somewhat different effect. The first disables CGI scripts and will already be set in many cases. Similarly, the second disables PHP. As to the third and fourth, they control how file content is processed by default. The final one allows a list of specific extensions to be unregistered from special processing (and thus revert to the default, which is often a verbatim copy over HTTP). Note that there are some other loosely related options not listed, due to obsolescence or redundancy.

Options -ExecCGI combined with either of the SetHandler lines is sufficient. The reason why all of these are listed is that in some cases one does not have access to the main Apache config file in order to apply the changes. Indeed, in shared server setups that is usually true. When some directives have been disabled at a higher level, there is no choice but to use what is available in an .htaccess file. Even if the main configuration is unviewable, it will be obvious when an option is disabled due to the server errors on attempting to access anything in that directory.

As a conclusion, behold this thoroughly commented htaccess file. It was tested against Apache 2.2, but should work in all 2.x versions. Feel free to use it as you wish.

# ============================================================================
# This .htaccess file is for setting sane restrictions on a publicly writable
#   directory.  As with all htaccess files, it will affect sub-directories.
# WARNING: Set this file to read-only [!]
#          Store in a read-only directory [!!]
# ============================================================================

# Do not allow files larger than 512 KiB .
LimitRequestBody 524288

# Deny access to all files by default.
# This will also prevent listing directory contents.
Order Allow,Deny

# Whitelist certain extensions.
<FilesMatch "\.(txt|css|jpg|png|gif)$" >
  Order Deny,Allow

# Prevent Apache from Executing Arbitrary PHP Scripts by
#   Clearing Handlers and MIME Types 
# Apache may be configured using one or more of these directives (or similar):
# -----------------------------------------
# AddType     application/x-httpd-php  .php
# DefaultType application/x-httpd-php  .php
# ForceType   application/x-httpd-php  .php
# AddHandler  application/x-httpd-php  .php
# AddHandler  cgi-script               .php
# AddHandler  php-script               .php
# AddHandler  [OTHER_HANDLER_NAME]     .php
# SetHandler  [SOME_HANDLER_NAME]      .php
# -----------------------------------------
# The purpose behind using a directive like this is to enable automatic parsing
#   of PHP (or similar) scripts.
# Using "AddType" is wrong.
#   (1) Sets a MIME type seen by clients. (bad)
#   (2) No need to set PHP MIME type; plain text or HTML works fine.
#   (3) For security, PHP files should not be served by default [!]
#   (4) AddType dates all the way back to 1996; it's time to remove it.
# The "DefaultType" and "ForceType" lines are even worse.  These set the
#   default MIME for everything to PHP scripts.  Very bad, and very hackish.
#   Use URL re-writing if you're trying to establish clean URLs.
# The AddHandler/SetHandler directives are correct in that they accomplish
#   what was intended.  Unfortunately, there's a little-known 'feature' in
#   Apache's implementation of these directives.  They match on any file
#   with such an extension present, including files with multiple or compound
#   extensions like ".php.txt" or ".php.jpg" .  This is a code execution
#   vulnerability allowing PHP scripts to run while masquerading as a different
#   file type. [!!]  Any similar scripting languages that have an apache parser
#   loaded and do not require filesystem execute (+x) permission to run also
#   provide potential attack surfaces. [!]
# This risk can be mitigated in one of three ways:
#   (1) Sanitize all upload filenames with server code. (moderate)
#   (2) Validate uploaded file content with server code. (hard)
#   (3) Disable script execution. (easy)
# Our approach here is #3, which works for public upload directories intended
#   to store data files, not executables.  The vast majority of public upload
#   or public storage directories meet this criteria.
# Usual methods to disable script execution include these directives:
#   [*] Options -ExecCGI
#   [*] php_flag engine off
#   [*] SetHandler none
# Use as many of these you can; shared servers may disable some or all.
# In case none of those work, there are still two lesser options:
#   [*] SetHandler default-handler
#   [*] RemoveHandler .cgi .php .php3 .php4 .php5 .phtml .pl .py .pyc .pyo (...)
# The default handler is usually an HTTP file serve without any processing,
#   which works fine.  If the default handler is a parser, though, it will
#   not help.
# RemoveHandler works in most setups, except that you're never completely
#   sure you caught all the scripting extensions.  If you can view the apache
#   config file, check there for ones you missed.  Otherwise, just specify
#   as many plausible extensions as you can.
SetHandler default-handler

Related materials: