CGI Security

CGI security: hardening server-side scripts against common attacks

What are the most important security measures for a CGI script?

The most critical CGI security measures are: never passing user input to shell commands without strict validation and escaping; enabling taint mode in Perl to force explicit untainting of external data; validating all input against an allowlist of acceptable patterns before using it; storing scripts outside the web root where possible; running the CGI process as a low-privilege user; and keeping the interpreter and any libraries current.

Request custom development Getting started

Command injection: the highest-severity CGI risk

Command injection happens when a script passes user-supplied input to a shell command without properly escaping it. If your Perl script does system('sendmail ' . $email) and an attacker submits 'nobody@example.com; rm -rf /tmp/*', the shell expands the semicolon as a command separator and runs both commands. The fix in Perl is to use the list form of system() and exec(), which bypasses the shell entirely: system('sendmail', $email). In Python, use subprocess with a list argument rather than a shell string, and never pass shell=True with user-supplied data.

Any CGI script that calls an external program is a candidate for command injection. Audit every system(), exec(), open(), and backtick call in your scripts. If you cannot avoid passing user data to a shell command, use a module that handles escaping for you (like String::ShellQuote in Perl) and validate the input against a strict allowlist before passing it at all.

Perl taint mode

Perl's taint mode, enabled with the -T flag in the shebang line (#!/usr/bin/perl -T), marks all data from external sources (environment variables, form input, file reads) as 'tainted'. Using tainted data in any potentially dangerous operation (passing it to system(), writing it to a file path, using it in a regex that affects the program's behavior) causes a runtime error. You untaint data by matching it against a regex and extracting the captured group; the act of matching a literal regex and capturing is Perl's mechanism for saying 'I have validated this input'.

Taint mode is not a replacement for careful security thinking, but it is a useful safety net that prevents a whole class of accidents where tainted data reaches a dangerous call without explicit validation. It is especially valuable in CGI scripts because all form input arrives through the environment and standard input, which Perl taints by default when -T is active. Enable it on every CGI script you write.

Input validation: allowlist over blocklist

Validate input against what you expect to receive, not against a list of things you want to block. A blocklist approach tries to enumerate every dangerous character or pattern and strip or reject them; attackers are creative about finding patterns you did not think of. An allowlist approach says 'this field must match /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ or it is rejected', which is narrow and leaves no gap for unexpected input.

For each input field, define the type (email, integer, alphanumeric slug, etc.), maximum length, and any other structural constraints, then reject anything that does not fit. Do this before doing anything with the data, not as an afterthought. Log rejected inputs so you can see what is being attempted against your scripts.

Environment variable risks

CGI scripts receive a large set of environment variables from the web server, including HTTP_*, QUERY_STRING, PATH_INFO, and others. These are user-controlled (an attacker can send arbitrary HTTP headers, which become HTTP_* variables) and must be treated as untrusted. The PATH environment variable is particularly dangerous: if PATH includes . (the current directory) or world-writable directories, a setuid CGI script can be tricked into running an attacker's program instead of a system command.

Always set PATH explicitly at the top of your CGI scripts to a known-good value listing only the directories that contain the binaries you intend to call. Do not rely on the inherited PATH from the web server process. In Perl under taint mode this is enforced: taint mode refuses to use a tainted PATH for shell commands and requires you to set it explicitly.

Safe temporary files

CGI scripts that write to temporary files must use a method that avoids race conditions. On Unix, the traditional mktemp approach (generate a name, check if it exists, create it) has a TOCTOU (time-of-check to time-of-use) race where an attacker can create a symlink between the check and the create. In Perl use File::Temp, which opens the file atomically. In Python use tempfile.NamedTemporaryFile or tempfile.mkstemp, both of which create the file atomically and return a file descriptor.

Store temporary files in a directory only the web server user can write to, not /tmp directly if your system has a sticky-bit directory shared by all users. Set permissions so the file is readable only by the owner. Delete temporary files promptly after use, or register a cleanup function that runs even if the script exits early due to an error.

What to know

Key points

Get help

Listings and a local agent, when you are ready

Need a CGI script built, debugged, or a hosting environment configured? Forms use a clearly-marked placeholder endpoint until wired to a real system.

Hosting partner CGI-compatible hosting for CGI security

Reserved for an affiliate link to a vetted shared or VPS hosting provider that supports CGI and Perl/Python execution. Operator wires affiliate link here.

Development Need a custom CGI script built?

Self-hosted lead form for custom CGI development inquiries. Placeholder endpoint until wired to the operator's CRM or email handler.

Open inquiry form →

Custom CGI development

No spam. Your inquiry is answered within 1 business day.

Hosting setup help

No spam. Your inquiry is answered within 1 business day.

Questions

Frequently asked questions

What is command injection in a CGI script?
Command injection occurs when user-supplied input is passed to a shell command without proper escaping, allowing an attacker to append their own shell commands. For example, if a script calls system('sendmail ' . $email) and the email field contains a semicolon followed by a destructive command, the shell runs both. Use the list form of system() in Perl or subprocess with a list in Python to bypass the shell entirely.
What does Perl's taint mode do?
Taint mode, enabled with the -T flag in the shebang line, marks all data from external sources as tainted. Using tainted data in potentially dangerous operations causes a runtime error. You untaint it by matching it against a literal regex and extracting the capture, which forces you to explicitly validate the data. It is a safety net that catches accidental use of unvalidated external input in dangerous operations.
Should I validate input with an allowlist or a blocklist?
Use an allowlist. Define what valid input looks like for each field (an email address pattern, a numeric range, an alphanumeric slug) and reject anything that does not match. Blocklists try to enumerate dangerous patterns and always have gaps that attackers find. An allowlist leaves no room for unexpected input because you accept only what you explicitly expect.
How do I safely use temporary files in a CGI script?
Use a library that creates temporary files atomically: File::Temp in Perl, or tempfile.NamedTemporaryFile / tempfile.mkstemp in Python. These open the file and return a handle in a single atomic operation, avoiding the TOCTOU race in check-then-create. Store temp files in a directory only the web server user can write to, and delete them promptly after use.

CustomCGI publishes reference information on CGI programming and server-side web scripting for educational and informational purposes. Code examples are provided as-is for learning; always audit and test scripts in a staging environment before deploying to production. Security vulnerabilities in CGI scripts are a real risk; follow current best practices and review the security guide before deploying any user-facing code.