Software 44956 Published by

Chizhong Jin released v0.15.2 of Nginx CGI, fixing two bugs that could leave requests hung or leak memory. The hang fix resolves an issue where request bodies larger than client_body_buffer_size would stall indefinitely once the in-memory buffer was drained. The memory-safety patch closes a missing null-termination gap in constant cgi_set_var values, a follow-up to work from the previous release. Admins running CGI in production, especially those handling large POST bodies or constant config variables, should upgrade to close both gaps.



Nginx CGI v0.15.2 Ships With Hang and Memory-Safety Fixes

The module that lets you run CGI scripts through Nginx just fixed two bugs that could hang your server or leak memory.

On October 2, 2026, open-source developer Chizhong Jin (who publishes under the GitHub handle @pjincz) released v0.15.2 of nginx-cgi, a third-party module that adds CGI support to Nginx and its Angie fork. It's a small release by volume, but the two fixes it carries are the kind you only notice when something has already broken. Jin handled both changes himself.

There is no larger feature set here. No new directives, no speed bumps, no refactor. Just two targeted patches, each with its own commit, and packaged for both Debian and Fedora. That's honest work, even if it doesn't make for exciting headlines.

Perl

The hang that wasn't really a hang

The first fix addresses a request-body hang that is easy to misdiagnose. Nginx doesn't slurp an entire POST body into memory at once. It reads the body in small chunks governed by directives like client_body_buffer_size. When a request body exceeds that buffer, the data arrives in pieces: some sits in already-allocated buffers, the rest trickles in over the socket.

nginx-cgi feeds that body to the CGI script through the script's stdin. The module pumps whatever buffers happen to be available into the pipe, then waits for the next read event before sending more. The bug, spotted by @RichardSteele in issue #25 back on August 24, was that once the available buffers were drained, the module kept waiting for a new read edge that might never come. So the script blocked forever on read(0, ...). The request hung.

Not the interesting part: CONTENT_LENGTH still reported the correct full size. So your logs say the body was 512 KB and everything looks fine, while the request simply never finishes. A reporter's strace confirmed the process was stuck in that read call.

Jin's fix adds five lines to ngx_http_cgi_module.c. After draining the current buffers, if the stdin pipe still exists, Nginx is still reading the body, and the connection's read event is ready, the module now re-queues the event instead of idling. The commit message even quotes the reporter's suggested check, so Jin credited the upstream bug report rather than quietly papering over it.

This is a real availability bug, not a theoretical one. If your CGI endpoints take large POST bodies, file uploads, form submissions, streaming payloads, and anything above the body buffer size would hang indefinitely before this patch. To prove the fix, Jin added a regression test that posts a 512 KB body through a base64-encoding script and checks that the full, correct output comes back.

The memory-safety gap that keeps resurfacing

The second fix deals with something C programmers live and die by: null termination.

The cgi_set_var <name> <value> directive lets you inject custom environment variables into a CGI script, and it can embed Nginx variables like $http_host. When the value is a constant string in the config rather than a dynamically allocated buffer, the resulting C string wasn't guaranteed to end in a \0 byte. Read past the end of an unterminated string and you get a buffer over-read. That can leak data, corrupt values, or crash.

This one has been circling the project. Contributor @friendlyanon first addressed null termination in cgi_set_var via PR #24, merged August 3 for v0.15.1. Then @friendlyanon reopened the concern in issue #26, this September 25, because the constant-value case was still not terminated properly. v0.15.2 closes that remaining gap.

It's small. It's the kind of change that gets zero cheers. But memory-safety hardening like this is exactly what separates a hobby module from something you'd run on a box that matters.

Why this fits a bigger pattern

v0.15.2 didn't show up in a vacuum. The project has been leaning hard into security-hardening over the past year, and both fixes are part of that arc.

The biggest prior move came in v0.15 (March 5, 2026), which added cgi_stderr for logging control and quietly deprecated the REMOTE_USER and AUTH_TYPE environment variables "for security reasons." The reasoning was subtle. In Nginx, the $remote_user variable and HTTP basic-auth share a code path. When $remote_user appears anywhere in your config, Nginx parses the request's Authorization header and stores the decoded username internally and without actually requiring authentication. Since nginx-cgi exposed that field as REMOTE_USER, an assumed-authenticated identity could leak into scripts that trusted it.

Jin called it "a serious vulnerability" in issue #22. v0.15 removed those variables by default and stopped exposing the raw Authorization header out of the box. If you genuinely need it, you can opt back in with a single line:

cgi_set_var HTTP_AUTHORIZATION $http_authorization;

The null-termination fix sits right alongside that philosophy. Constant config values are user-influenced, so they get handled in memory like they should.

What CGI actually is, and why Nginx shied away from it

If you've never dug into this, it helps to understand what the module is doing. CGI is one of the oldest standards on the web, defined formally in RFC 3875. It describes how a web server hands an incoming request to an external program like a script written in shell, Python, Perl, Ruby, C, or just about anything executable, and how that program returns a response.

The contract is deliberately blunt. The server passes request metadata to the script through environment variables (REQUEST_METHOD, QUERY_STRING, HTTP_ACCEPT, REMOTE_ADDR, and dozens more). It feeds the request body over stdin. The script writes its response to stdout in a two-part format: a header section, an empty separator line, then the body.

The catch is that CGI spawns a fresh process for every single request. That makes it trivial to program against and a nightmare at scale. That trade-off is exactly why stock Nginx, that was built for high concurrency on an event-driven architecture, never shipped native CGI support. People fell back to proxies, FastCGI, or third-party modules instead.

nginx-cgi closes that gap by compiling as a dynamically loadable shared object (ngx_http_cgi_module.so, packaged as libnginx-mod-http-cgi). You load it with Nginx's standard load_module directive and enable it per location block with a plain cgi on; statement.

To understand where CGI actually fits, Jin's own benchmark is worth a look. He ran it on a cheap $5/month Vultr instance, one shared vCPU around 2.3 GHz, 1 GB of RAM, Debian 12, using ab -n 1000 -c 100. The results put CGI at roughly 1,007 requests per second against a shell script that served static text at nearly 4,891 requests per second.

That's about one-fifth static serving speed. But it still handles roughly a thousand simple requests per second on hardware that costs less than a streaming subscription, which blows past the "a few requests per minute" myth that lingers around CGI. The real limitations are process-spawn overhead and latency variability, not an absolute ceiling. It's a rather inefficient way to serve dynamic content, though the small memory footprint and setup simplicity make it a reasonable fit for specific jobs.

Jin is candid about the sweet spot. CGI suits low-frequency apps, system-management panels, resource-limited or embedded systems, personal websites, and prototyping. It does not suit high QPS, high traffic, or high concurrency.

If you're keeping score on the module's standing, it's not a star magnet. Roughly 57 stars and 5 forks. It doesn't need them. It fills a gap that a lot of high-traffic shops ignore but plenty of small teams and legacy deployments depend on.

Installation and the roadmap

The v0.15.2 release reinforces the multi-distribution support the project has built up:

Debian and Ubuntu users can pull pre-built libnginx-mod-http-cgi packages from the GetPageSpeed repository, covering Debian 12/13 and Ubuntu 20.04/22.04/24.04 on both amd64 and arm64, with sudo apt-get install nginx nginx-module-cgi. Fedora builds are driven by a spec file whose version field was bumped to 0.15.2. Angie users can grab the module straight from its official repo. You can also build locally with ./build-deb-package.sh or compile from source using --add-dynamic-module=$PWD/../nginx-cgi.

The full directive set is comprehensive: cgi on|off, cgi pass, cgi_interpreter, cgi_working_dir, cgi_body_only, cgi_path, cgi_strict, cgi_set_var, cgi_stderr, cgi_rdns, and cgi_timeout. The module also handles streaming for both request and response bodies, picking between one-shot, chunked, or full streaming output depending on how fast your script produces data.

Sandboxing is documented for chroot, Docker, and FreeBSD jails, though Jin warns that chroot alone isn't a real security boundary since it still shares kernel space, and recommends containers or jails instead.

Five issues remain open as of this release. #26 is the null-termination fix this version already addresses. The rest point at the near-term roadmap: stderr output disappearing during process exit (#21), a proposed cgi_detailed_error for client-facing error reporting (#11), X-Accel-Redirect support (#9), and a fully RFC-compliant PATH_TRANSLATED (#7). That reads as a roadmap focused on error reporting, RFC completeness, and deeper Nginx integration.

The commit history, 209 commits at this point, is almost entirely Chizhong Jin, with community help from @friendlyanon on null-termination and @dvershinin on package documentation. That's a lot of solo maintenance on a module that quietly keeps a chunk of the web's legacy dynamic content alive.

For anyone running cgi on; in production, upgrading to v0.15.2 is recommended, especially if your deployments accept request bodies larger than client_body_buffer_size or use cgi_set_var with constant values.

Head here to the release page for v0.15.2 and the full repository README if you want the diffs and the complete changelog.