CGI scripts and Server Side Includes on Apache httpd 2.4

Updated November 2026

CGI lets Apache run a program for each request and send back whatever the program prints. Server Side Includes (SSI) let Apache fill small directives inside HTML pages, such as a shared footer or a date, before sending them. Both still work in httpd 2.4 and are fine for small jobs, but they belong to the web of the 1990s: for anything busy or exposed, use FastCGI or an application server.

How CGI works

The Common Gateway Interface (RFC 3875) is a contract between the web server and a program. For every request, httpd starts the program, passes request details as environment variables (REQUEST_METHOD, QUERY_STRING, CONTENT_LENGTH, REMOTE_ADDR and others), feeds any POST body on standard input, and reads the response from standard output. The output must start with at least a Content-Type header and a blank line, then the body.

The program can be written in any language: Python, Perl, shell, compiled C. That simplicity is the appeal. The catch is that a new process starts for every single request.

mod_cgi or mod_cgid

Two modules do the same job and accept the same directives:

Most current Linux packages use the event MPM, so you will normally want mod_cgid:

# Debian/Ubuntu: picks cgid automatically with a threaded MPM
sudo a2enmod cgi
# Your MPM seems to be threaded. Selecting cgid instead of cgi.
sudo apache2ctl -t && sudo systemctl restart apache2

# RHEL/Fedora: /etc/httpd/conf.modules.d/01-cgi.conf already loads
# mod_cgid (threaded MPM) or mod_cgi (prefork)
sudo apachectl -M | grep cgi

Both distributions also ship a ready ScriptAlias: Debian maps /cgi-bin/ to /usr/lib/cgi-bin/ (in conf-available/serve-cgi-bin.conf, enabled by default), RHEL and Fedora map it to /var/www/cgi-bin/.

A ScriptAlias directory

ScriptAlias maps a URL prefix to a directory and treats every file in it as a program. Keeping scripts in one place, outside the DocumentRoot, is the safest layout: nobody can upload a file into it through the website, and the source can never be served as plain text.

ScriptAlias "/cgi-bin/" "/srv/example/cgi-bin/"
<Directory "/srv/example/cgi-bin">
    AllowOverride None
    Options None
    Require all granted
</Directory>

<IfModule cgid_module>
    CGIDScriptTimeout 20
</IfModule>

CGIDScriptTimeout (mod_cgid, since 2.4.10) kills a script that produces no output for 20 seconds; mod_cgi has the equivalent CGIScriptTimeout since 2.4.59. Without them, the general Timeout applies.

A first script, /srv/example/cgi-bin/hello.py, that handles both GET and POST. Python's old cgi module was removed in Python 3.13, so it uses urllib.parse, and it escapes the input before printing it into HTML:

#!/usr/bin/env python3
import html
import os
import sys
from urllib.parse import parse_qs

if os.environ.get("REQUEST_METHOD") == "POST":
    length = int(os.environ.get("CONTENT_LENGTH") or 0)
    data = sys.stdin.buffer.read(length).decode("utf-8", "replace")
else:
    data = os.environ.get("QUERY_STRING", "")

name = parse_qs(data).get("name", ["world"])[0]

print("Content-Type: text/html; charset=utf-8")
print()
print(f"<p>Hello, {html.escape(name)}!</p>")
sudo chmod 755 /srv/example/cgi-bin/hello.py
curl "http://www.example.com/cgi-bin/hello.py?name=Ann"
# <p>Hello, Ann!</p>
curl -d "name=Bob" http://www.example.com/cgi-bin/hello.py
# <p>Hello, Bob!</p>

CGI outside cgi-bin

To run scripts from a normal content directory you need two things: Options +ExecCGI and a handler for the file extension.

<Directory "/var/www/example/tools">
    Options +ExecCGI
    AddHandler cgi-script .cgi
</Directory>

Never do this for a directory where users or a web application can upload files. Anyone who can drop an executable .cgi file there can run commands on your server. The same directives work in .htaccess only if AllowOverride permits Options and FileInfo.

When a CGI script fails

What you see and what it usually means
SymptomCause and fix
The browser shows or downloads the script's sourceCGI is not enabled for that path: the module is not loaded, or there is no ScriptAlias/ExecCGI + handler.
403 ForbiddenA Require rule, or file and directory permissions. Check error_log.
500 and malformed header from script 'x': Bad header: ...The script printed something before the headers, or forgot the blank line after them.
500 and AH01241: exec of '...' failed plus End of script output before headersThe file is not executable (chmod 755), the #! line points to a missing interpreter, or the file has Windows line endings (\r after the interpreter name).
Works in your shell, fails under httpdDifferent user and PATH: the script runs as the web server user (www-data or apache). Use full paths to programs and files.

Run the script by hand first (./hello.py, not python3 hello.py, so the #! line is tested). Then read the error log. For stubborn cases, ScriptLog logs/cgi_log records the request headers, the script's output and its standard error for every failed run. It is meant for debugging only; switch it off afterwards. More general fixes are in troubleshooting errors.

CGI security

Server Side Includes

SSI directives look like HTML comments, for example <!--#include virtual="/includes/footer.shtml" -->. mod_include processes them through the INCLUDES output filter. If SSI is not enabled, they stay in the page as comments: invisible in the browser, but visible in the source.

# Debian/Ubuntu (RHEL/Fedora load mod_include in conf.modules.d/00-base.conf)
sudo a2enmod include
<Directory "/var/www/example">
    Options +IncludesNOEXEC
</Directory>
AddType text/html .shtml
AddOutputFilter INCLUDES .shtml
DirectoryIndex index.shtml index.html

IncludesNOEXEC enables SSI but disables #exec, which runs shell commands with the web server's permissions. Use plain Includes only if you need #exec and nobody untrusted can edit pages. Note that #include virtual can still run CGI scripts from ScriptAlias directories under IncludesNOEXEC.

A page index.shtml using the common elements:

<!--#include virtual="/includes/header.shtml" -->
<p>Search term: <!--#echo encoding="entity" var="QUERY_STRING" --></p>

<!--#config timefmt="%B %e, %Y" -->
<p>Page updated <!--#echo var="LAST_MODIFIED" --></p>

<!--#if expr='-R "10.0.0.0/8"' -->
  <p><a href="/intranet/">Staff pages</a></p>
<!--#endif -->

<!--#include virtual="/cgi-bin/hello.py?name=SSI" -->
<!--#include virtual="/includes/news.shtml" onerror="/includes/no-news.html" -->

Avoid parsing every .html file for SSI: httpd would scan pages that contain no directives. If you cannot rename files, XBitHack on parses only files with the execute bit set (chmod +x page.html); XBitHack full also sends a Last-Modified header. SSI pages normally get no Last-Modified or ETag, which hurts caching; SSILastModified on or mod_expires headers help.

Why both are legacy, and what to use

CGI is not deprecated in httpd 2.4, and the documentation still describes it in full, but it is rarely the best choice for new work. Even the CGI how-to points Python authors to a WSGI framework for anything more complex. A CGI process starts, loads its interpreter and libraries, opens database connections, answers one request and exits. Under load that overhead dominates, and every concurrent request is a whole process. SSI is cheap, but it is a tiny template language evaluated on every request for content that rarely changes.

If you move to nginx, it has an SSI module but no CGI support at all; old CGI scripts there need a FastCGI wrapper such as fcgiwrap.

Sources