Software 44903 Published by

A new version of the open-source Nginx CGI module is available, fixing a crash in the cgi_set_var directive caused by unterminated string values bleeding into adjacent memory. The bug produced silent 500 errors and only surfaced under AddressSanitizer, since the project's normal test harness masks the out-of-bounds read. The fix took shape across three GitHub accounts, friendlyanon reported it, and package maintainer dvershinin refined the patch against stock Nginx 1.30.0 after the initial version both failed to compile and introduced its own buffer overflow. The release also rides on the security-focused v0.15, which deprecated the REMOTE_USER variable, and it offers native CGI support across Linux, macOS, BSD, and Solaris without requiring a reverse proxy.



Nginx CGI v0.15.1 ships after a three-part battle over a single missing null byte

A two-line patch to a 30-year-old web standard came together from a package maintainer, a sanitizer, and a few second guesses.

The newest release of the pjincz/nginx-cgi module fixes a crash in the cgi_set_var directive, and it does so in a way that arguably shouldn't have worked. Released today as v0.15.1, the patch is essentially two lines of C. Yet it was shaped by a package maintainer, a user with a memory sanitizer, and a couple of arguments before it ever merged.

For those unbothered by how it got there, here's what actually changed. cgi_set_var now null-terminates the values it injects into a CGI script's environment. Before this, an unterminated string bled into adjacent memory and turned perfectly normal requests into silent 500s.

Perl

What is Nginx CGI anyway?

CGI is 33 years old, give or take. Introduced in 1993, the Common Gateway Interface lets a static HTTP server hand a request off to an external program, capture its output, and relay it back to a browser. For most of the 1990s and early 2000s, it was simply the default way people built contact forms and early content management systems.

It also has a reputation problem. Every request spawns a fresh operating-system process, and that overhead scales terribly. The industry quietly moved on to faster things: PHP-FPM, FastCGI, WSGI servers, Node.js. CGI earned its name as the slow tech to avoid.

However, at the same time, that model still fits a real niche. Low-traffic sites. System panels. Embedded devices. Quick prototypes. As the nginx-cgi README puts it, CGI is "neither a demon nor an angel. It is simply a tool."

pjincz/nginx-cgi takes that philosophy literally. Maintainer Chizhong Jin (@pjincz) built it as a native, RFC 3875–compliant dynamic module with no reverse-proxy hacks and no daemon tacked on the side. It ships as a loadable .so, tested across Linux, macOS, FreeBSD, OpenBSD, and Solaris. Windows? Not supported, for the blunt reason that "nginx barely supports Windows."

The trouble starts with a symptom that looks completely benign. A user called friendlyanon runs a Prometheus exporter on nginx-cgi and found that adding a single cgi_set_var line killed every request.

$ curl -s -v http://127.0.0.1:9101/metrics
< HTTP/1.1 500 Internal Server Error
< Content-Length: 0

No body. No error-log line. No systemd entry. That silence is the tell, and it points at a memory-safety violation rather than a logic error. The crash swallows itself instead of failing loudly. And it only happened when cgi_set_var met the newer cgi pass form.

The root cause is C being C. Strings need a null terminator so execve and friends know where a value ends. This module builds a NAME=value string by hand in memory, and somewhere along the way it forgot to write that byte. The result: a variable meant to arrive as CONSTVAR=hello showed up as CONSTVAR=hellocgi_set_var, trailing config text bleeding in.

Here's the part that made it easy to miss. The project's own test harness compiles with a debug flag that gives each memory allocation its own page. That isolation masks out-of-bounds reads, so the normal suite passes even with the bug present. The only thing that surfaces it is AddressSanitizer, which instruments memory access and screams the instant you over-read a buffer.

What makes this release worth reading is the process, not just the patch. PR #24 turned into a three-way back-and-forth with each person bringing different expertise. friendlyanon had already patched their own Arch Linux package locally and confirmed a fix, then opened issue #23 to push it upstream properly. The key insight came early: passing an unterminated string to a system API is undefined behavior, "so I wouldn't really bet on this being caught without the test harness being run with sanitizers."

Then dvershinin, who maintains the Debian, Ubuntu, and RPM packages, checked out the branch and ran it against stock Nginx 1.30.0. The initial patch failed on two counts. It didn't compile, with a char * assignment through a u_char ** tripping Nginx's -Werror checks. And it introduced its own heap-buffer-overflow: a helper peeked one byte past a string to check for an existing terminator, and that read went out of bounds on every single request. Under ASAN, that's 22 failures.

dvershinin spotted the deeper trap as well. Even when the over-read byte happened to be zero, the function assumed the string was already terminated. The next pool allocation then overwrote it, reproducing the exact "appended text" symptom. The fix was to drop the peek entirely and always copy the buffer with an explicit terminator. Add a small cast. That delivered 225 of 225 tests passing under ASAN, and the variable now reaching the child process cleanly as CONSTVAR=hello.

The maintainer took the hit gracefully. "You guys are right, I really did a mistake here. I really should add some CI for this repo, I will have a try later."

That admission matters, honestly, because the repo has no continuous integration. That's precisely why a compile error and a memory regression slipped through unnoticed until an external contributor ran the harness with strict flags.

For what it's worth, there's a timing quirk worth flagging. The fix merged into main on August 3, but the release tag didn't land until September 22. Seven weeks apart. Apparently dvershinin asked for a formal release, and nobody cut one until then. Until September, you needed the AUR package, a GetPageSpeed repo, or a manual build off main to get the fix at all.

v0.15.1 sits on top of a bigger v0.15 release from March, and that one carried its own drama. It deprecated the REMOTE_USER and AUTH_TYPE variables after a security hole surfaced in issue #22. Because Nginx shares auth-parsing code between its basic-auth module and the CGI module, even referencing $remote_user in your config could leak the variable into a script's environment with no actual authentication. The maintainer called it "a serious vulnerability." So the Authorization header stays hidden from CGI scripts by default unless you opt in explicitly through cgi_set_var.

The newer patch doesn't touch that work at all. It closes a separate hole in the custom-variable path. Together, the two releases sketch a module caught between adding features and hardening itself.

On performance, a common assumption is that CGI handles a handful of requests per minute, and the author pushes back. Running a benchmark on a $5/month Vultr instance (one shared ~2.3 GHz vCPU, 1 GB RAM, Debian 12), CGI hit about 1,007 requests per second against a CGI endpoint, while the same server served nearly 4,891 requests per second for a static file. Not the same. But it's nowhere near a trickle.

It's a reasonably respectable number for constrained hardware, though the author is clear that CGI is not built for high QPS or high concurrency. Process overhead is the ceiling. If you're running something modest, 1,000 req/s is plenty. If you're not, you're probably reaching for FastCGI anyway.

Not a flashy release. But it's the kind you want to see in a small open-source project. The crash it fixes would likely have lingered forever in an untested path had the package maintainer not pulled the branch, installed it, and run it under a sanitizer on stock software. That's the ecosystem doing exactly what it's supposed to do: scrutiny from people who actually deploy the stuff.

The missing CI stays a genuine risk. Compile errors and memory regressions will keep surfacing only when someone manually runs the tests with strict flags, and the maintainer has only signaled intent to fix it. That's a fair bit of faith to place in a promise.

Still, for anyone running low-traffic CGI behind Nginx, v0.15.1 is worth the update. It closes a real crash with a code path that's actually been tested under strict conditions.

If you want to try it, the release is tagged now. Grab the changelog and source straight from GitHub, or install the module through GetPageSpeed's APT repo or the Angie repository, where it ships as nginx-module-cgi under a permissive 2-clause BSD license.

Head here to the GitHub release page.