CGI scripts and Server Side Includes on Apache httpd 2.4
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:
- mod_cgid for threaded MPMs (event, worker). It runs a separate daemon that forks the CGI processes, because forking from a multi-threaded server is expensive. It talks to that daemon over a Unix socket set by
ScriptSock. - mod_cgi for the prefork MPM and for Windows.
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
| Symptom | Cause and fix |
|---|---|
| The browser shows or downloads the script's source | CGI is not enabled for that path: the module is not loaded, or there is no ScriptAlias/ExecCGI + handler. |
| 403 Forbidden | A 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 headers | The 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 httpd | Different 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
- Every script is a program on your server, running as the web server user with that user's file access. Treat all input as hostile: validate it, never pass it to a shell, and escape it before writing it into HTML.
- The environment is attack surface. Shellshock (CVE-2014-6271, 2014) let attackers run code through Bash via crafted environment variables, and the CVE record names mod_cgi and mod_cgid as a vector. Keep interpreters patched, not only httpd.
- Passwords stay hidden by default. httpd does not pass the
Authorizationheader to scripts;CGIPassAuth On(2.4.13 and later) changes that only where a script really does its own HTTP authentication. Request headers whose names contain underscores or other invalid characters are not passed at all. - suEXEC can run scripts as a different user per virtual host (
SuexecUserGroup) or per home directory. It is a setuid-root helper with strict checks, and the httpd documentation warns not to use it unless you understand it. On Debian it comes in theapache2-suexec-pristineorapache2-suexec-custompackage;apachectl -VshowsSUEXEC_BIN.
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" -->
- Included files are only parsed if they match your filter rule too. A footer named
footer.htmlis included as plain text, and any SSI inside it is left unprocessed; name itfooter.shtml. virtualvsfile:virtualtakes a URL path on the same server and goes through normal access control;filetakes a path relative to the current directory and cannot use../or absolute paths. Prefervirtual.- Conditions use the 2.4 expression syntax:
-Rtests the client IP against a network, andv("name")reads a variable set with#set. Old 2.2-style expressions such asexpr="$QUERY_STRING = /x/"needSSILegacyExprParser on. - Encode output.
#echoapplies HTML entity encoding by default; keep it that way for anything that comes from the request. onerror(2.4) shows another document if an include fails, instead of "[an error occurred while processing this directive]".
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.
- FastCGI with mod_proxy_fcgi. A long-running process pool answers many requests. PHP-FPM is the common case (see PHP with Apache), and any FastCGI application works the same way:
ProxyPass "/app/" "fcgi://127.0.0.1:4000/". - mod_fcgid, an httpd subproject that starts and manages FastCGI processes itself. Its latest release is 2.3.9 from October 2013 (as of November 2026); Debian packages it as
libapache2-mod-fcgid. - Application servers behind mod_proxy: Python (WSGI/ASGI servers), Node.js, Java, over HTTP with mod_proxy_http, or uwsgi and SCGI with mod_proxy_uwsgi and mod_proxy_scgi. See reverse proxy.
- Instead of SSI: build shared headers and footers once with a static site generator or your application's templates, and serve plain files.
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
- Dynamic Content with CGI, Apache HTTP Server 2.4 documentation
- Introduction to Server Side Includes, Apache HTTP Server 2.4 documentation
- mod_cgi, Apache HTTP Server 2.4 documentation
- mod_cgid, Apache HTTP Server 2.4 documentation
- mod_include, Apache HTTP Server 2.4 documentation
- Core features (Options, CGIPassAuth), Apache HTTP Server 2.4 documentation
- .htaccess files tutorial (CGI example), Apache HTTP Server 2.4 documentation
- suEXEC support, Apache HTTP Server 2.4 documentation
- New features in Apache HTTP Server 2.4 (header translation, SSI onerror)
- mod_proxy_fcgi, Apache HTTP Server 2.4 documentation
- mod_fcgid, Apache HTTP Server Project
- Download page, Apache HTTP Server Project (mod_fcgid 2.3.9)
- CVE-2014-6271 (Shellshock), CVE Program
- Debian apache2 postinst (default modules and serve-cgi-bin)
- Debian a2enmod script (cgi to cgid selection)
- apache2-suexec-pristine, Debian packages
- Fedora httpd 01-cgi.conf
- Fedora httpd 00-base.conf
- Fedora httpd.conf (ScriptAlias)
- ngx_http_ssi_module, nginx documentation