Introduction
A broken string escaping routine in Nginx Proxy Manager allowed any authenticated user with certificate management permissions to achieve root level remote code execution through a simple shell injection payload in the DNS credentials field. For the tens of thousands of self hosted deployments relying on this tool as their primary reverse proxy, the vulnerability quietly turned a routine certificate configuration workflow into a full container compromise path.
Nginx Proxy Manager is an open source, Docker based application that provides a web interface for managing Nginx reverse proxy hosts, SSL certificates via Let's Encrypt, and access control lists without requiring users to edit Nginx configuration files directly. With over 33,000 GitHub stars and 3,800 forks, it is one of the most popular tools in the self hosting ecosystem, widely deployed in homelabs and small organizations as the front door to their web services.
Technical Information
Vulnerability Classification
CVE-2026-40519 is classified under CWE-78: Improper Neutralization of Special Elements used in an OS Command. It carries a CVSS v3.1 score of 7.5 (High). The vulnerability affects Nginx Proxy Manager versions 2.9.14 through 2.15.1.
Root Cause: Broken Sanitization Order
The vulnerability resides in the setupCertbotPlugins() async function within backend/setup.js. This function is responsible for writing DNS provider credentials to disk so that Certbot can use them during DNS-01 challenge certificate issuance. The function attempts to sanitize user supplied dns_provider_credentials before embedding the value into a shell command template executed via utils.exec(), which wraps Node.js child_process.exec().
Here is the vulnerable code that was removed in the fix:
// REMOVED — vulnerable code const escapedCredentials = certificate.meta.dns_provider_credentials .replaceAll("'", "\\'") .replaceAll("\\", "\\\\"); const credentials_cmd = `[ -f '${credentials_loc}' ] || { mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo '${escapedCredentials}' > '${credentials_loc}' && chmod 600 '${credentials_loc}'; }`; promises.push(utils.exec(credentials_cmd));
The critical flaw is the order of the two replaceAll() calls. Single quotes are escaped first (replacing ' with \'), and then backslashes are escaped second (replacing \ with \\). This ordering is fatally incorrect. When an attacker supplies input like x' ; cmd ; #, the following sequence occurs:
- The first
replaceAllescapes the single quote: the input becomesx\' ; cmd ; # - The second
replaceAllthen doubles the backslash that was just introduced by the first step: the input becomesx\\' ; cmd ; #
The result is that \\ is interpreted by the shell as a literal backslash character, and the single quote that follows it is now unescaped, breaking out of the quoted string context. The attacker's cmd then executes as an independent shell command, and the trailing # comments out the remainder of the original command.
Attack Flow
Exploitation follows a straightforward sequence:
-
Authentication: The attacker authenticates to the Nginx Proxy Manager web interface with an account that holds
certificates:managepermissions. This could be a dedicated low privilege account that was granted only certificate management access. -
Payload storage: The attacker configures a DNS-01 challenge certificate and stores a malicious payload in the
dns_provider_credentialsfield. The payload takes the formx' ; <arbitrary_command> ; #. -
Trigger via restart: Upon backend restart (which occurs automatically during certificate processing or container restart), the
setupCertbotPlugins()function reads the storeddns_provider_credentialsvalue from the database, applies the broken sanitization, and interpolates the result into a shell command string passed tochild_process.exec(). -
Command execution: The shell (
/bin/sh -c) interprets the injected command. Because Nginx Proxy Manager typically runs as root in standard Docker deployments, the injected command executes with root privileges inside the container.
The vulnerability is particularly reliable in Docker environments where the /etc/letsencrypt/credentials/ directory is not persisted across container restarts, because setupCertbotPlugins() is called each time the backend initializes to recreate the credential files.
Impact
Successful exploitation grants the attacker root level access to the container, enabling:
- Access to secrets stored in the container, including TLS private keys and Let's Encrypt credentials
- Access to the Nginx Proxy Manager SQLite database containing all configuration and user data
- Service disruption through container manipulation
- Persistence through backdoor installation within the container
- Potential lateral movement if the container has network access to other services
Patch Information
The fix for CVE-2026-40519 was merged on June 7, 2026, via commit a5db5ed, incorporating Pull Request #5498 ("fix: Changed order of escape to prevent RCE") submitted by researcher Yasha-ops and merged by the project maintainer jc21. The vulnerability had been initially disclosed via GitHub issue #5478 on April 12, 2026.
The patch touches a single file, backend/setup.js, with 5 additions and 7 deletions. While the PR title and original issue suggested simply reversing the order of replaceAll() calls, the maintainer chose a fundamentally stronger approach: eliminating the shell command execution path entirely.
The replacement code uses Node.js's built in fs/promises module:
// ADDED — patched code import fs from "fs/promises"; // ... if (typeof certificate.meta.dns_provider_credentials === "string") { promises.push( fs.mkdir("/etc/letsencrypt/credentials", { recursive: true }) .then(() => fs.writeFile( credentials_loc, certificate.meta.dns_provider_credentials, { mode: 0o600, flag: "wx" } )) .catch((err) => { if (err.code !== "EEXIST") throw err; }) ); }
This rewrite is notable for several reasons:
No shell involvement at all. By switching from child_process.exec() to fs.mkdir() and fs.writeFile(), user controlled data never touches a shell interpreter. This eliminates the entire class of OS command injection, not just the specific bypass caused by the escaping order.
Equivalent functionality preserved. The { recursive: true } flag on fs.mkdir() mirrors the mkdir -p behavior. The { mode: 0o600 } option on fs.writeFile() replicates the chmod 600 step. The { flag: "wx" } flag makes the write fail if the file already exists, mimicking the original [ -f ... ] || guard, and the .catch() handler gracefully ignores EEXIST errors to ensure idempotency.
Defense in depth. Even if a future code change accidentally alters the flow, the credentials string can never be interpreted as executable code because it is only ever passed as data to a file write API.
The following table summarizes the changes:
| Aspect | Vulnerable Code | Fixed Code |
|---|---|---|
| Credential writing method | child_process.exec() with shell interpolation | fs.writeFile() with direct data argument |
| Directory creation | Shell mkdir via exec() | fs.mkdir({ recursive: true }) |
| Input sanitization | String replacement of quotes/backslashes (broken order) | Not needed; no shell interpretation |
| File permissions | Default | mode: 0o600 (owner read/write only) |
| File creation mode | Overwrite possible | flag: "wx" (exclusive create) |
The commit is GPG signed via GitHub and was cleanly merged into the develop branch. Users running any version from v2.9.14 through v2.15.1 should update to a release that includes this commit or apply the patch manually to backend/setup.js.
This fix serves as a textbook example of the canonical CWE-78 mitigation: string based escaping within shell command templates is fundamentally fragile. Replacing shell execution with native API calls eliminates the entire class of injection risk.
Affected Systems and Versions
Nginx Proxy Manager versions 2.9.14 through 2.15.1 are affected. The vulnerability specifically impacts deployments where:
- Any user account holds the
certificates:managepermission - DNS-01 challenge certificates are configured (or can be configured by the attacker)
- The backend restarts at any point after the malicious payload is stored (which is common in Docker environments where credential directories are not persisted)
The vulnerability is fixed in any build that includes commit a5db5ed156355e3088e7d1ceb0533d4bae922def. The primary distribution channel is the Docker image jc21/nginx-proxy-manager on Docker Hub.
Vendor Security History
Nginx Proxy Manager has accumulated a concerning pattern of security issues over its lifetime. According to OpenCVE, the project has 7 tracked CVEs. Notable prior vulnerabilities include:
| CVE | Description |
|---|---|
| CVE-2025-50579 | CORS misconfiguration allowing unauthorized domain access to JWT tokens |
| CVE-2023-35843 | Prior authenticated command injection in certificate.js |
| Untracked | XSS vulnerability in domain input field (versions <= v2.9.16) |
A container vulnerability scan reported in GitHub Issue #3503 revealed over 400 vulnerabilities in the official Docker image, with a substantial number classified as critical (CVSS above 9.0). The issue was tagged with bug and stale labels, and no direct maintainer response was documented.
The project has also faced recurring community concerns about its maintenance status. A Reddit discussion captured user concerns that the developer had stated they were not going to work on version 2 further. Development has continued, but the pattern suggests a reactive rather than proactive security posture: critical, publicly reported flaws get attention, while systemic security debt accumulates without response. Organizations relying on Nginx Proxy Manager should factor this pattern into their risk calculations.
References
- NVD: CVE-2026-40519
- VulnCheck Advisory: Nginx Proxy Manager Authenticated RCE via setupCertbotPlugins()
- GitHub Commit a5db5ed (Fix)
- GitHub Pull Request #5498: fix: Changed order of escape to prevent RCE
- GitHub Issue #5478: RCE via Shell Injection in DNS Credentials
- Nginx Proxy Manager GitHub Repository
- CWE-78: Improper Neutralization of Special Elements used in an OS Command
- CISA Known Exploited Vulnerabilities Catalog
- VulnCheck: Quantifying 2026 Routinely Targeted Vulnerabilities (So Far)
- OpenCVE: Nginx Proxy Manager CVEs
- GitHub Issue #3503: Security problems. Many critical vulnerabilities
- GitHub API: Commit a5db5ed
- GitHub API: Issue #5498
- GitHub API: Issue #5478



