CVE-2026-107807: Nginx UI: Node Secret Credential Exposure via URL Query Parameter

Published Oct 9, 2026
·
Updated

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 (?nodesecret=), 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 nodesecret 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.ini on disk (readable by any process with filesystem access) - The NGINXUICLUSTERNODE environment variable (visible in ps aux, Docker inspect, Kubernetes pod specs, CI/CD logs) - The SQLite database nodes table as the token column 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?nodesecret=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 ?nodesecret= URL directly | Anyone with browser access | | HTTP Referer header | Page with ?nodesecret= in URL links to external resource | Third-party servers | | WebSocket URL logs | ws://host/api/ws?nodesecret=xxx logged by proxies | Proxy log readers | | app.ini on disk | Cluster node URLs contain nodesecret= | Filesystem readers | | Environment variables | NGINXUICLUSTERNODE=...&nodesecret=... | 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:

bash grep -oP 'nodesecret=[^&\s"]+' /var/log/nginx/access.log Output: nodesecret=a1b2c3d4-e5f6-7890-abcd-ef1234567890

Step 3 — Use the extracted secret for full API access:

http 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.

http 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:

yaml environment: - NGINXUICLUSTERNODE=http://10.0.0.1:9000?name=node1&nodesecret=my-node-secret&enabled=true

Step 2 — Extract the secret from the URL query string.

Step 3 — Authenticate to the target node:

http 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 ?nodesecret= in the URL.

Step 2 — That page contains a resource (image, script, analytics) from a third-party domain.

Step 3 — Browser sends:

http GET /analytics.js HTTP/1.1 Host: analytics.third-party.com Referer: https://nginx-ui.internal/api/settings?nodesecret=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("nodesecret"). Present in the current dev branch. The cluster URL format with embedded nodesecret has been present since v2.0.0-beta.23.

---

7. Recommended Fixes

Fix 1 (Primary) — Remove query parameter support for nodesecret:

go // 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 websocketorigin.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:

ini [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) go // 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("nodesecret") }

File: internal/middleware/middleware.go (L96-103) go // 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) go 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/websocketorigin.go (L39-46) go func isTrustedNodeRequest(r http.Request) bool { secret := strings.TrimSpace(r.Header.Get("X-Node-Secret")) if secret == "" { secret = strings.TrimSpace(r.URL.Query().Get("nodesecret")) }

return secret != "" && secret == settings.NodeSettings.Secret }

File: docs/guide/config-server.md (L109-111) markdown 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) text [cluster] Node = http://10.0.0.1:9000?name=node1&nodesecret=my-node-secret&enabled=true Node = http://10.0.0.2:9000?name=node2&nodesecret=my-node-secret&enabled=true Node = http://10.0.0.3?name=node3&nodesecret=my-node-secret&enabled=true

File: internal/cluster/cluster.go (L55-73) go 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("nodesecret"), Enabled: u.Query().Get("enabled") == "true", }

return

File: internal/kernel/boot.go (L132-144) go 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) } }

Other sources

Nginx UI is a web user interface for the Nginx web server. From 2.0.0 until 2.5.0, Nginx UI accepts the Node.Secret master credential through the nodesecret query parameter in HTTP and WebSocket authentication paths instead of requiring the X-Node-Secret header. The credential can consequently appear in access logs, proxy logs, browser history, Referer headers, configuration URLs, and deployment environment data. A party that obtains the secret can bypass normal password, JWT, session, and second-factor checks and obtain persistent administrative API access, including access to configuration and secret material. This issue is fixed in version 2.5.0.

— MITRE

Affected Software

2 affected componentsFixes available
Nginx UI Nginx UI>=2.0.0<2.5.0
go/github.com/0xJacky/Nginx-UI>=1.9.10-0.20250517140552-daee3ac7ade1<1.9.10-0.20260728074433-a3999bd78a3b
1.9.10-0.20260728074433-a3999bd78a3b

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade go/github.com/0xJacky/Nginx-UI to a version that resolves this vulnerability.

    Fixed in 1.9.10-0.20260728074433-a3999bd78a3b
  2. Upgrade

    Upgrade nginx-ui to a version that resolves this vulnerability.

    Fixed in 2.5.0
  3. Configuration

    Remove support for node_secret in URL query parameters and accept the credential only through the X-Node-Secret header; apply the same change to isTrustedNodeRequest in websocket_origin.go.

    nginx-ui authentication middleware node_secret query-parameter authentication = disabled
  4. Configuration

    Redesign the cluster node configuration format so node URLs do not embed secrets in query parameters; use a separate configuration key or store secrets in a separate secrets file with restricted permissions.

    nginx-ui cluster node configuration node_secret storage in cluster URLs = separate configuration key or restricted-permission secrets file
  5. Configuration

    Encrypt node tokens at rest using the CryptoSettings.Secret key before storing them in the SQLite database.

    nginx-ui SQLite nodes table nodes.token = encrypted with CryptoSettings.Secret

Event History

Oct 9, 2026
CVE Published
via MITRE·03:04 PM
Data Sourced
via MITRE·03:04 PM
DescriptionSeverityWeakness
Advisory Published
via GitHub·05:07 PM
Data Sourced
via GitHub·05:07 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which deployments are affected?

Nginx UI versions from 2.0.0 up to, but not including, 2.5.0 are affected. Version 2.5.0 contains the fix.

2

What does an attacker need to exploit this issue?

An attacker needs to obtain the Node.Secret master credential, such as from access or proxy logs, browser history, Referer headers, configuration URLs, or deployment environment data. Possession of that secret allows authentication through affected HTTP and WebSocket paths.

3

What access does a leaked Node.Secret provide?

The secret can bypass normal password, JWT, session, and second-factor checks and provide persistent administrative API access. This includes access to configuration and secret material.

4

How can teams identify potential exposure before upgrading?

Review access logs, proxy logs, configuration URLs, browser-related records, and deployment environment data for occurrences of the node_secret query parameter or the Node.Secret value. Treat any exposed value as compromised because it can be used for administrative API access.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203