{"url":"https://api.github.com/repos/NginxProxyManager/nginx-proxy-manager/issues/5498","repository_url":"https://api.github.com/repos/NginxProxyManager/nginx-proxy-manager","labels_url":"https://api.github.com/repos/NginxProxyManager/nginx-proxy-manager/issues/5498/labels{/name}","comments_url":"https://api.github.com/repos/NginxProxyManager/nginx-proxy-manager/issues/5498/comments","events_url":"https://api.github.com/repos/NginxProxyManager/nginx-proxy-manager/issues/5498/events","html_url":"https://github.com/NginxProxyManager/nginx-proxy-manager/pull/5498","id":4292266064,"node_id":"PR_kwDOBtnUP87TxtQv","number":5498,"title":"fix: Changed order of escape to prevent RCE","user":{"login":"Yasha-ops","id":54901715,"node_id":"MDQ6VXNlcjU0OTAxNzE1","avatar_url":"https://avatars.githubusercontent.com/u/54901715?v=4","gravatar_id":"","url":"https://api.github.com/users/Yasha-ops","html_url":"https://github.com/Yasha-ops","followers_url":"https://api.github.com/users/Yasha-ops/followers","following_url":"https://api.github.com/users/Yasha-ops/following{/other_user}","gists_url":"https://api.github.com/users/Yasha-ops/gists{/gist_id}","starred_url":"https://api.github.com/users/Yasha-ops/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Yasha-ops/subscriptions","organizations_url":"https://api.github.com/users/Yasha-ops/orgs","repos_url":"https://api.github.com/users/Yasha-ops/repos","events_url":"https://api.github.com/users/Yasha-ops/events{/privacy}","received_events_url":"https://api.github.com/users/Yasha-ops/received_events","type":"User","user_view_type":"public","site_admin":false},"labels":[{"id":1247018512,"node_id":"MDU6TGFiZWwxMjQ3MDE4NTEy","url":"https://api.github.com/repos/NginxProxyManager/nginx-proxy-manager/labels/awaiting%20feedback","name":"awaiting feedback","color":"e8d910","default":false,"description":""}],"state":"closed","locked":false,"assignees":[],"milestone":null,"comments":6,"created_at":"2026-04-19T23:04:04Z","updated_at":"2026-06-14T18:19:38Z","closed_at":"2026-06-07T22:51:46Z","assignee":null,"author_association":"CONTRIBUTOR","issue_field_values":[],"type":null,"active_lock_reason":null,"draft":false,"pull_request":{"url":"https://api.github.com/repos/NginxProxyManager/nginx-proxy-manager/pulls/5498","html_url":"https://github.com/NginxProxyManager/nginx-proxy-manager/pull/5498","diff_url":"https://github.com/NginxProxyManager/nginx-proxy-manager/pull/5498.diff","patch_url":"https://github.com/NginxProxyManager/nginx-proxy-manager/pull/5498.patch","merged_at":"2026-06-07T22:51:46Z"},"body":"## Summary\r\nNginx Proxy Manager is vulnerable to **authenticated remote code execution** due to a shell injection in `setupCertbotPlugins()` (`backend/setup.js`).\r\n\r\nThe user-controlled field `dns_provider_credentials` is interpolated into a shell command executed via `child_process.exec()` without proper escaping.\r\nAn attacker with `certificates:manage` permission can inject arbitrary commands, executed on backend restart (typically as `root` in Docker deployments).\r\n\r\n## Affected Versions\r\n```\r\nv2.9.14   v2.9.15   v2.9.16   v2.9.17   v2.9.18   v2.9.19   v2.9.20\r\nv2.9.21   v2.9.22\r\nv2.10.0   v2.10.1   v2.10.2   v2.10.3   v2.10.4\r\nv2.11.0   v2.11.1   v2.11.2   v2.11.3\r\nv2.12.0   v2.12.1   v2.12.2   v2.12.3   v2.12.4   v2.12.5   v2.12.6\r\nv2.13.0   v2.13.1   v2.13.2   v2.13.3   v2.13.4   v2.13.5   v2.13.6   v2.13.7\r\nv2.14.0   (latest, still vulnerable)\r\n```\r\n\r\n## Root Cause\r\nIncorrect escaping order:\r\n```javascript\r\nconst escapedCredentials = certificate.meta.dns_provider_credentials\r\n    .replaceAll(\"'\", \"\\\\'\")\r\n    .replaceAll(\"\\\\\", \"\\\\\\\\\");\r\n```\r\n\r\nBackslashes are escaped **after** single quotes, breaking the protection and allowing injection.\r\nExample input:\r\n```\r\nx' ; cmd ; #\r\n```\r\n\r\nResults in command execution:\r\n```bash\r\necho 'x\\\\' ; cmd ; #'\r\n```\r\n\r\n## Exploitation\r\n1. Store payload in `dns_provider_credentials` (DB or race condition)\r\n2. Restart backend\r\n3. Payload executes via `/bin/sh -c` inside `exec()`\r\n\r\nReliable in Docker since `/etc/letsencrypt/credentials/` is not persisted.\r\nExample payload:\r\n```\r\nfake' > /dev/null; CMD ; echo '\r\n```\r\n\r\n## Impact\r\n* Full RCE\r\n* Access to secrets, TLS keys, database\r\n* Service disruption and persistence\r\n\r\n## Fix\r\n```javascript\r\n.replaceAll(\"\\\\\", \"\\\\\\\\\")\r\n.replaceAll(\"'\", \"\\\\'\");\r\n```","closed_by":{"login":"jc21","id":1518257,"node_id":"MDQ6VXNlcjE1MTgyNTc=","avatar_url":"https://avatars.githubusercontent.com/u/1518257?v=4","gravatar_id":"","url":"https://api.github.com/users/jc21","html_url":"https://github.com/jc21","followers_url":"https://api.github.com/users/jc21/followers","following_url":"https://api.github.com/users/jc21/following{/other_user}","gists_url":"https://api.github.com/users/jc21/gists{/gist_id}","starred_url":"https://api.github.com/users/jc21/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jc21/subscriptions","organizations_url":"https://api.github.com/users/jc21/orgs","repos_url":"https://api.github.com/users/jc21/repos","events_url":"https://api.github.com/users/jc21/events{/privacy}","received_events_url":"https://api.github.com/users/jc21/received_events","type":"User","user_view_type":"public","site_admin":false},"reactions":{"url":"https://api.github.com/repos/NginxProxyManager/nginx-proxy-manager/issues/5498/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"timeline_url":"https://api.github.com/repos/NginxProxyManager/nginx-proxy-manager/issues/5498/timeline","performed_via_github_app":null,"state_reason":null}