Summary
Nginx UI: Node Secret Credential Exposure via URL Query Parameter
1. Vulnerability Summary
nginx-ui's Node.Secret is a master credential that bypasses all JWT/password authentication for the entire API. The application accepts this credential via a URL query parameter (?node_secret=), causing it to be recorded in plaintext in HTTP access logs, reverse proxy logs, browser history, and HTTP Referer headers. Additionally, the official cluster configuration format embeds node secrets directly into URL query strings stored in app.ini and environment variables, creating a systemic credential exposure pattern across the entire cluster deployment model.
An attacker who gains read access to any log aggregation system, proxy log, or configuration file can extract the node secret and obtain full, persistent, unauthenticated administrative access to the nginx-ui API, including reading TLS private keys, modifying nginx configurations, and (when chained with Bug #1) achieving OS-level code execution.
2. Root Cause Analysis
2.1 Node Secret Accepted as URL Query Parameter
The getNodeSecret function reads the credential from the URL query string as a fallback when the X-Node-Secret header is absent: 1
This function is called in both AuthRequired() and AuthRequiredWS() middleware, meaning the query parameter bypass works for all authenticated HTTP and WebSocket endpoints: 2 3
The same pattern is repeated in the WebSocket origin checker, which also reads node_secret from the URL: 4
2.2 Node Secret Is a Full Authentication Bypass
The documentation explicitly states this is by design: 5
When the secret matches, the middleware sets the request context to an admin-level init user and calls c.Next(), bypassing all JWT validation, session checks, and 2FA: 2
2.3 Cluster Configuration Embeds Node Secrets in URLs
The official cluster configuration format, documented and used in app.example.ini, stores node secrets as URL query parameters: 6
The parseNodeUrl function extracts the secret from the URL's query string and stores it in the database as the node's Token field: 7
This means node secrets are embedded in:
app.inion disk (readable by any process with filesystem access)- The
NGINX_UI_CLUSTER_NODEenvironment variable (visible inps aux, Docker inspect, Kubernetes pod specs, CI/CD logs) - The SQLite database
nodestable as thetokencolumn in plaintext
2.4 Node Secret Generation Uses UUID
The secret is auto-generated as a UUID v4 if not set: 8
UUID v4 has 122 bits of entropy, which is adequate. However, the exposure surface described in this report makes entropy irrelevant, the secret is leaked through operational channels, not brute-forced.
3. Exposure Surface
The following table maps each exposure vector to its source in the codebase:
| Vector | How It Happens | Who Can See It |
|---|---|---|
| HTTP access logs | GET /api/settings?node_secret=xxx logged by nginx/caddy/apache |
Log readers, SIEM operators |
| Application logs | Gin debug mode logs full request URLs | Server operators, log aggregators |
| Browser history | Admin uses ?node_secret= URL directly |
Anyone with browser access |
| HTTP Referer header | Page with ?node_secret= in URL links to external resource |
Third-party servers |
| WebSocket URL logs | ws://host/api/ws?node_secret=xxx logged by proxies |
Proxy log readers |
app.ini on disk |
Cluster node URLs contain node_secret= |
Filesystem readers |
| Environment variables | NGINX_UI_CLUSTER_NODE=...&node_secret=... |
ps aux, Docker inspect, K8s pod specs |
| CI/CD pipeline logs | Env vars printed during deployment | CI/CD log viewers |
| Database | nodes.token column stored in plaintext SQLite |
DB file readers |
4. Proof of Concept
Scenario A: Log-Based Secret Extraction
Step 1, Attacker gains read access to nginx access logs (e.g., via a misconfigured log aggregator, a compromised monitoring account, or a separate vulnerability).
Step 2, Search logs for the pattern:
grep -oP 'node_secret=[^&\s"]+' /var/log/nginx/access.log
# Output: node_secret=a1b2c3d4-e5f6-7890-abcd-ef1234567890
Step 3, Use the extracted secret for full API access:
GET /api/settings HTTP/1.1
Host: target:9000
X-Node-Secret: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Response: Full settings JSON including JwtSecret, NodeSecret, all nginx paths, and all configured credentials.
GET /api/nginx/config?filepath=/etc/nginx/nginx.conf HTTP/1.1
Host: target:9000
X-Node-Secret: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Response: Full nginx configuration including any embedded credentials.
Scenario B: Environment Variable Exposure in Docker/Kubernetes
Step 1, Attacker reads a Kubernetes pod spec or Docker Compose file:
environment:
- NGINX_UI_CLUSTER_NODE=http://10.0.0.1:9000?name=node1&node_secret=my-node-secret&enabled=true
Step 2, Extract the secret from the URL query string.
Step 3, Authenticate to the target node:
POST /api/nginx/test HTTP/1.1
Host: 10.0.0.1:9000
X-Node-Secret: my-node-secret
The attacker now has full admin access to the cluster node.
Scenario C: Referer Header Leak to Third-Party
Step 1, Admin navigates to a page with ?node_secret= in the URL.
Step 2, That page contains a resource (image, script, analytics) from a third-party domain.
Step 3, Browser sends:
GET /analytics.js HTTP/1.1
Host: analytics.third-party.com
Referer: https://nginx-ui.internal/api/settings?node_secret=a1b2c3d4-...
The third-party server receives the node secret in the Referer header.
5. Impact
- Confidentiality: Full read access to all nginx configurations, TLS private keys, ACME account credentials, database contents, and all settings stored in
app.ini. - Integrity: Full write access to all nginx configurations across all cluster nodes. An attacker can deploy malicious nginx configs, disable TLS, or redirect traffic.
- Availability: An attacker can reload or restart nginx with a broken configuration, causing a denial of service.
- Persistence: The node secret does not expire and has no revocation mechanism. Once leaked, it provides permanent access until manually rotated.
- Cluster-wide blast radius: A single leaked node secret from one cluster member's logs can be used to authenticate to any other node that shares the same secret.
6. Affected Versions
All versions of nginx-ui where getNodeSecret reads from c.Query("node_secret"). Present in the current dev branch. The cluster URL format with embedded node_secret has been present since v2.0.0-beta.23.
7. Recommended Fixes
Fix 1 (Primary), Remove query parameter support for node_secret:
// internal/middleware/middleware.go
func getNodeSecret(c *gin.Context) (secret string) {
// Only accept via header, never via query parameter
return c.GetHeader("X-Node-Secret")
}
Apply the same change to isTrustedNodeRequest in websocket_origin.go.
Fix 2, Redesign cluster node configuration format:
The cluster node URL format must not embed secrets in query parameters. Use a separate configuration key:
[cluster]
Node = http://10.0.0.1:9000?name=node1&enabled=true
NodeKey1 = <secret-for-node1>
Or store secrets in a separate secrets file with restricted permissions.
Fix 3, Encrypt node tokens at rest:
The nodes.token column in the SQLite database stores secrets in plaintext. Encrypt using the CryptoSettings.Secret key before storage.
Fix 4, Add secret rotation support:
Provide an API endpoint to rotate the Node.Secret and invalidate all existing sessions authenticated via the old secret.
8. Timeline
| Date | Event |
|---|---|
| 2026-04-21 | Vulnerability identified via source code review |
| , | Vendor notification (pending) |
| , | CVE assignment (pending) |
Citations
File: internal/middleware/middleware.go (L73-80)
// getNodeSecret from header or query
func getNodeSecret(c *gin.Context) (secret string) {
if secret = c.GetHeader("X-Node-Secret"); secret != "" {
return secret
}
return c.Query("node_secret")
}
File: internal/middleware/middleware.go (L96-103)
// Check node secret authentication
if nodeSecret := getNodeSecret(c); nodeSecret != "" && nodeSecret == settings.NodeSettings.Secret {
initUser := user.GetInitUser(c)
c.Set("Secret", nodeSecret)
c.Set("user", initUser)
c.Next()
return
}
File: internal/middleware/middleware.go (L152-158)
if nodeSecret := getNodeSecret(c); nodeSecret != "" && nodeSecret == settings.NodeSettings.Secret {
initUser := user.GetInitUser(c)
c.Set("Secret", nodeSecret)
c.Set("user", initUser)
c.Next()
return
}
File: internal/middleware/websocket_origin.go (L39-46)
func isTrustedNodeRequest(r *http.Request) bool {
secret := strings.TrimSpace(r.Header.Get("X-Node-Secret"))
if secret == "" {
secret = strings.TrimSpace(r.URL.Query().Get("node_secret"))
}
return secret != "" && secret == settings.NodeSettings.Secret
}
File: docs/guide/config-server.md (L109-111)
This secret is used to authenticate the communication between the Nginx UI servers.
Also, you can use this secret to access the Nginx UI API without a password.
File: app.example.ini (L45-48)
[cluster]
Node = http://10.0.0.1:9000?name=node1&node_secret=my-node-secret&enabled=true
Node = http://10.0.0.2:9000?name=node2&node_secret=my-node-secret&enabled=true
Node = http://10.0.0.3?name=node3&node_secret=my-node-secret&enabled=true
File: internal/cluster/cluster.go (L55-73)
func parseNodeUrl(nodeUrl string) (node *model.Node, err error) {
u, err := url.Parse(nodeUrl)
if err != nil {
return
}
var sb strings.Builder
sb.WriteString(u.Scheme)
sb.WriteString("://")
sb.WriteString(u.Host)
sb.WriteString(u.Path)
node = &model.Node{
Name: u.Query().Get("name"),
URL: sb.String(),
Token: u.Query().Get("node_secret"),
Enabled: u.Query().Get("enabled") == "true",
}
return
File: internal/kernel/boot.go (L132-144)
func InitNodeSecret() {
if settings.NodeSettings.Secret == "" {
logger.Info("Secret is empty, generating...")
uuidStr := uuid.New().String()
err := settings.Update(func() {
settings.NodeSettings.Secret = uuidStr
})
if err != nil {
logger.Error("Error save settings", err)
}
logger.Info("Generated Secret: ", uuidStr)
}
}
Impact
CVE-2026-107807 has a CVSS score of 8.8 (High). The vector is network-reachable, low privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (1.9.10-0.20260728074433-a3999bd78a3b); upgrading removes the vulnerable code path.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Already deployed Kodem?
See it in your environmentNew to Kodem? Get a demo →Remediation advice
Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.
Frequently Asked Questions
- What is CVE-2026-107807? CVE-2026-107807 is a high-severity security vulnerability in github.com/0xJacky/Nginx-UI (go), affecting versions >= 1.9.10-0.20250517140552-daee3ac7ade1, < 1.9.10-0.20260728074433-a3999bd78a3b. It is fixed in 1.9.10-0.20260728074433-a3999bd78a3b.
- How severe is CVE-2026-107807? CVE-2026-107807 has a CVSS score of 8.8 (High). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which versions of github.com/0xJacky/Nginx-UI are affected by CVE-2026-107807? github.com/0xJacky/Nginx-UI (go) versions >= 1.9.10-0.20250517140552-daee3ac7ade1, < 1.9.10-0.20260728074433-a3999bd78a3b is affected.
- Is there a fix for CVE-2026-107807? Yes. CVE-2026-107807 is fixed in 1.9.10-0.20260728074433-a3999bd78a3b. Upgrade to this version or later.
- Is CVE-2026-107807 exploitable, and should I be worried? Whether CVE-2026-107807 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2026-107807 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2026-107807? Upgrade
github.com/0xJacky/Nginx-UIto 1.9.10-0.20260728074433-a3999bd78a3b or later.