Servers, proxies and network services
nginx-ui restricted-directive bypass (CVE-2026-84316, CVE-2026-84317, CVE-2026-84318, CVE-2026-84319)
nginx-ui restricted-directive bypass check
nginx-ui is a web interface for managing nginx. Its security control, ValidateConfigFile, refuses configuration that contains directives able to run code or raise privileges: load_module of the njs or Perl modules, js_import, js_content, perl_*, and user root. Four advisories published on 12 October 2026 describe ways around that control for a logged-in user. Where nginx loads the njs, Perl or Lua module, which the official Docker image does, the injected configuration runs code in the nginx worker.
| CVE | CVSS | What |
|---|---|---|
| CVE-2026-84316 | 8.8 | The validator split the configuration on newlines and checked only the first token of each line. nginx separates directives with ;, so a harmless directive first and a restricted one after a semicolon on the same line passed |
| CVE-2026-84317 and CVE-2026-84318 | 8.8 each | The MCP tool nginx_config_add wrote the caller's configuration to disk and reloaded nginx without calling ValidateConfigFile at all. The REST paths do call it. Two identifiers describe this one gap, with different affected ranges (84318 from 2.0.0-rc.6) |
| CVE-2026-84319 | 8.3 | The MCP configuration interface let a secondary authenticated user write and apply the restricted directives the regular API refuses, such as loading the njs module and importing JavaScript, which runs njs in the nginx worker |
| Authentication | A logged-in user (a secondary user for 84319) |
| Affected | nginx-ui below 2.4.1 |
| Fixed | 2.4.1 |
| Exploited | No exploitation reported in the sources used |
How the script reads an install
The nginx-ui binary has no version flag, and its version endpoint needs a login, so a read-only check cannot ask it. The script uses what it can read:
NGINX_UI_VERSION, if you set it from the web interface, or the package version if there is one.- A string inside the binary. The 2.4.1 validator adds the names
lua_package_cpathandlua_package_pathto its restricted list. 2.4.0 does not contain them. A binary without them has the older validator, which is a strong finding. A binary with them is consistent with 2.4.1 or later, but it is a string match, so confirm the version in the web interface. - When the two disagree (a version of 2.4.1 or later on a binary without the names, or the reverse) the script says so rather than choosing.
The binary is found through NGINX_UI_BIN, the usual install paths, the ExecStart of the nginx-ui systemd unit, and running processes.
The official container image, uozi/nginx-ui, is checked too, by its tag in docker or podman. A tag that names a version (2.4.0, v2.4.1) is judged. A tag such as latest does not name a version and the version inside an image cannot be read from outside, so it is a warning that says so. An image on disk is not necessarily a running container. If no binary or image is found on the host but you have set NGINX_UI_VERSION (for an instance elsewhere), that version is judged on its own.
Quick start
curl -fLO https://raw.githubusercontent.com/PrivacySafe/security-patches/refs/heads/main/nginx-ui/nginx-ui-check.sh
less nginx-ui-check.sh
chmod +x nginx-ui-check.sh
./nginx-ui-check.sh check
./nginx-ui-check.sh hunt
Commands
| Command | What it does |
|---|---|
check (default) |
Finds nginx-ui (binary, container image, or a version you supply), reads the version and the binary signature, and reports. Exit 0 nothing exposed, 1 exposed or unclear |
hunt |
Reads nginx configuration for the semicolon-bypass shape and lists restricted directives present, for review. Exit 0 nothing found, 1 a strong lead |
version |
Prints the script version |
Env: NGINX_UI_BIN, NGINX_UI_VERSION, and NGINX_UI_CONF_DIRS (space separated nginx configuration directories for hunt; default /etc/nginx /etc/angie /usr/local/etc/nginx /usr/local/nginx/conf).
What hunt looks for
A restricted directive appearing after a semicolon on a line that already holds another directive, which is the shape of CVE-2026-84316, is a strong lead. Restricted directives in plain form are listed for review only, because legitimate njs and Perl use looks the same. The MCP paths (84317, 84318, 84319) write ordinary-looking directives, so a clean result does not rule them out: compare your configuration against your own records.
Update
Update nginx-ui to 2.4.1 or later and restart it. Until then the project suggests limiting who has an nginx-ui account, keeping it off untrusted networks, and not giving secondary users write access. This is PrivacySafe's recommendation.
If it was exposed
Review nginx configuration written since the exposure for restricted directives you did not add, remove them, reload nginx, rotate nginx-ui credentials and review who has accounts. If nginx loads the njs, Perl or Lua module, treat the worker as possibly compromised.
Sources
- nginx-ui 2.4.1 release
- GHSA-cf23-7qxj-xmhr (CVE-2026-84316)
- GHSA-76pm-mq2q-9gcr (CVE-2026-84317)
- GHSA-9jww-cxg5-rj9m (CVE-2026-84318)
Before you run it
- The fix. Update to nginx-ui 2.4.1. All four flaws need a logged-in user, and they matter most where nginx loads the njs, Perl or Lua module, as the official Docker image does.
- A read-only check cannot ask the program its version. The binary has no version flag and its version endpoint needs a login. The script judges the container image
uozi/nginx-uiby its tag when the tag names a version; a tag such aslatestdoes not, and is a warning. It also uses a version you supply inNGINX_UI_VERSION, the package version, and a string inside the binary that the 2.4.1 validator adds. - The binary check is a string match. It looks for names the fixed validator adds. It does not run the program.
- CVE-2026-84317 and CVE-2026-84318 describe one gap with different affected ranges, and 84318 starts at 2.0.0-rc.6.
- It also checks running docker and podman containers, unless you name a directory or set
SCAN_CONTAINERS=0. A read-only probe inside each running docker and podman container runs the same two signature tests this check applies to the binary (lua_package_cpathandlua_package_path) and copies only a small file recording the answer. The binary itself is never copied and nginx-ui is never run. A container is therefore judged by the signature alone (no version is read): a binary without the 2.4.1 validator names is reported as affected. The script runs once per container in isolation and does not repeat the host's discovery.
The Security Patches software and this documentation are dedicated to the public domain under CC0 1.0 Universal. Other pages on privacysafe.app/docs are CC BY-SA 4.0 unless the page says otherwise.