Digital Lab
Python CGI Lab
CGI scripts and Python backend logic, running live on ordinary shared hosting — no app server, no port to bind, no process to keep alive. Just Apache and a Python interpreter.
What CGI actually is
CGI — the Common Gateway Interface — is the oldest way to make a web server run a program instead of serving a static file. It dates to 1993. The "interface" part is almost insultingly simple: when a request hits a script, the web server (Apache, here) starts a brand-new process, hands it the request as environment variables plus the raw request body on stdin, and reads whatever the process prints to stdout as the response.
That's the entire protocol. No sockets to open, no framework required, no persistent process. A CGI script is just a program that knows it must:
- Print a
Content-Typeheader line - Print one blank line (the header/body separator — easy to forget, breaks everything if missing)
- Print the response body
That's hello.py → in its entirety: three print statements.
Why this still matters on shared hosting
Modern Python web apps usually run as a long-lived process — Gunicorn, uWSGI, an ASGI server — sitting behind a reverse proxy, listening on its own port. Cheap shared hosting (like this site's host, World4You) generally won't let you do that: no port binding, no persistent background process, no control over what's listening where.
What shared hosts do support — going back decades — is Apache configured to execute a script and capture its output. That's exactly the contract CGI provides, which is why it's still the practical way to run server-side Python on this kind of hosting. The tradeoff is that the Python interpreter starts cold on every single request — there's no warm process holding a database connection pool or cached state. For small scripts and low traffic, that startup cost is negligible. It would be a real problem at scale.
Statelessness — the core limitation
Because every request gets a fresh process with no memory of the last one, a CGI script by itself has no concept of "earlier" or "later." Anything that needs to persist — a visit count, a session, a queue of feedback messages — has to be written somewhere outside the process, almost always the filesystem on shared hosting (a database is the other option, where available). counter.py → demonstrates this directly: it reads a number from a text file, increments it, writes it back, with an OS-level file lock so two simultaneous requests can't corrupt the count by racing each other.
Reading a request: environment variables and stdin
Everything the browser sent arrives as environment variables — REQUEST_METHOD, QUERY_STRING, HTTP_USER_AGENT, and so on — readable with plain os.environ. A GET request's form data lives entirely in QUERY_STRING. A POST request's body has to be read from sys.stdin, exactly CONTENT_LENGTH bytes — read less and you lose data, read more (or read until EOF) and the script can hang waiting for bytes that will never arrive, since CGI doesn't close stdin for you. env.py → dumps the environment variables for whatever request loaded it; echo.py → parses a real form submission via both GET and POST.
Note: these examples parse the query string and POST body with urllib.parse rather than the stdlib cgi module. cgi.FieldStorage was the traditional tool for this, but the whole cgi module was deprecated in Python 3.11 and removed entirely in 3.13 (PEP 594) — worth knowing if you're maintaining older CGI code that imports it.
Security notes that actually matter here
CGI's simplicity cuts both ways — there's no framework quietly escaping things for you, so a few rules are worth stating plainly:
- Escape everything you echo back. Any user-supplied string printed into HTML is a potential XSS vector unless run through
html.escape()first — every script in this lab does this. - Never build a shell command from request data.
os.system()orsubprocesswithshell=Trueand unsanitized input is a direct path to remote code execution on the host. None of these scripts shell out at all. - Trust
CONTENT_LENGTH, not "read until done." Reading stdin without a length bound from an untrusted client risks both hangs and unbounded memory use. - File locking matters the moment two requests can land at once. A naive read-modify-write to a shared file (like a counter or a log) will silently corrupt data under concurrent load without a lock.