CVE-2026-84315: Github.com/0xJacky/Nginx UI Nginx UI OTP Secure-session Step-up Vulnerability

HIGH
Advisory Identifier: GHSA-5v7c-xpfp-p65m
CVSS 8.3
CWE-862
Published
Advisory Summary

CVE-2026-84315 is a HIGH (CVSS 8.3) nginx ui otp secure-session step-up vulnerability (CWE-862: Missing Authorization) affecting Github.com/0xJacky/Nginx UI libraries in the Go ecosystem. The issue involves nginx ui otp secure-session step-up is missing on sensitive mutation routes. Remediation is available by upgrading affected packages to version 1.9.10-0.20260703131214-23d1934099b0.

CVE-2026-84315 at a Glance

CVE Identifier CVE-2026-84315
GHSA Identifier GHSA-5v7c-xpfp-p65m
Severity HIGH
CVSS v3.1 Base Score 8.3
CWE Classification CWE-862: Missing Authorization
Affected Product Github.com/0xJacky/Nginx UI
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 8.3

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality Low (C:L)
Integrity High (I:H)
Availability High (A:H)

How to Fix CVE-2026-84315

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.

github.com/0xJacky/Nginx-UI fixed: 1.9.10-0.20260703131214-23d1934099b0
Upgrade github.com/0xJacky/Nginx-UI to 1.9.10-0.20260703131214-23d1934099b0
Verify Direct and Transitive Dependencies

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-84315, 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 1.9.10-0.20260703131214-23d1934099b0), 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-862: Missing Authorization, where the product does not perform an authorization check when an actor attempts to access a resource or perform an action. 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

Reporter: Jagadesh Achanta Target: `0xJacky/nginx-ui` Affected version / commit tested: - App version: `2.3.10` - Commit: `4948fba8744e82c3d7003f68ee164e9f49421066` - Disclosure channel: GitHub Private Vulnerability Reporting - Credential reference: `cred:github-jagstack-gh-cli` ## AI assistance disclosure This report was prepared by Jagadesh Achanta with assistance from OpenAI/Codex for public source review, local evidence collection, and report drafting. ## Summary Nginx UI has a short-lived OTP/passkey "secure session" step-up boundary for users with 2FA enabled. `RequireSecureSession()` rejects an OTP-enabled user's request unless the request includes a valid `X-Secure-Session-ID` issued after a fresh OTP/passkey verification. The project already applies this step-up middleware to sensitive operations such as: - Nginx config create/edit/rename/delete; - site save/delete/enable/disable/rename/duplicate; - stream save/delete/enable/disable/rename/duplicate; - settings save and protected secret reads; - backup restore. However, several similarly sensitive authenticated mutation routes remain outside `RequireSecureSession()`. An attacker who obtains an authenticated JWT/session for a 2FA-enabled user, but does not have the victim's fresh OTP/passkey secure-session token, can still perform those operations. The affected route groups include: - automatic backup create/modify/delete/restore/test-S3 operations; - DNS credential create/edit/delete operations; - certificate and ACME user create/modify/delete/register operations; - DNS domain, record, and DDNS create/update/delete operations; - Nginx reload/restart, stub-status toggle, performance-setting update, and module-cache refresh operations; - site/stream advanced-mode toggles and batch updates; - auto-certificate binding/unbinding for sites; - site navigation order and health-check updates. This report is distinct from my earlier Nginx UI report about auto-backup path traversal. This report is about missing 2FA step-up authorization on sensitive authenticated mutation routes. ## Why this is a security boundary `internal/middleware/secure_session.go` only requires a secure-session token for users with OTP enabled. For OTP users, it reads `X-Secure-Session-ID` from the header or query string and aborts with `401 Unauthorized` if the secure session is missing or invalid: ```go if !cUser.EnabledOTP() { c.Next() return } ssid := c.GetHeader("X-Secure-Session-ID") if ssid == "" { ssid = c.Query("X-Secure-Session-ID") } if ssid == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "message": "Secure Session ID is empty", }) return } if user.VerifySecureSessionID(ssid, cUser.ID) { c.Next() return } ``` `internal/user/otp.go` issues the secure-session ID for only 10 minutes: ```go func SetSecureSessionID(userId uint64) (sessionId string) { sessionId = uuid.NewString() cache.Set(secureSessionIDCacheKey(sessionId), userId, 10*time.Minute) return } ``` The UI/server already rely on this boundary for sensitive actions. For example, `api/config/router.go` wraps config mutations with `RequireSecureSession()`: ```go o := r.Group("", middleware.RequireSecureSession()) { o.POST("configs", AddConfig) o.POST("config", EditConfig) o.POST("config_mkdir", Mkdir) o.POST("config_rename", Rename) o.POST("config_delete", DeleteConfig) } ``` `api/sites/router.go` protects normal site mutations: ```go o := r.Group("", middleware.RequireSecureSession()) { o.POST("sites/:name/rename", RenameSite) o.POST("sites/:name/enable", EnableSite) o.POST("sites/:name/disable", DisableSite) o.POST("sites/:name", SaveSite) o.DELETE("sites/:name", DeleteSite) o.POST("sites/:name/duplicate", DuplicateSite) o.POST("sites/:name/maintenance", EnableMaintenanceSite) } ``` `api/streams/router.go` protects normal stream mutations, and `api/settings/router.go` protects secret reads/settings saves: ```go o := r.Group("", middleware.RequireSecureSession()) { o.POST("streams/:name", SaveStream) o.POST("streams/:name/rename", RenameStream) o.POST("streams/:name/enable", EnableStream) o.POST("streams/:name/disable", DisableStream) o.DELETE("streams/:name", DeleteStream) o.POST("streams/:name/duplicate", Duplicate) } ``` ```go r.GET("settings/protected", middleware.RequireSecureSession(), GetProtectedSetting) r.POST("settings", middleware.RequireSecureSession(), SaveSettings) ``` The repository's own tests also verify this expected behavior. `api/config/security_test.go`, `api/sites/security_test.go`, and `api/streams/security_test.go` each assert that OTP-enabled users receive `401 Unauthorized` when protected mutations are attempted without a secure session. ## Affected routes outside the step-up boundary The affected routes are under the authenticated private router: ```go g := root.Group("/", middleware.AuthRequired(), middleware.Proxy()) ``` but they do not add `middleware.RequireSecureSession()`. ### Automatic backup management `api/backup/router.go` protects manual restore: ```go r.POST("/restore", middleware.AuthRequired(), middleware.RequireSecureSession(), middleware.EncryptedForm(), RestoreBackup) ``` but the automatic backup management router has no secure-session middleware: ```go r.POST("/auto_backup", CreateAutoBackup) r.POST("/auto_backup/:id", ModifyAutoBackup) r.DELETE("/auto_backup/:id", DestroyAutoBackup) r.PATCH("/auto_backup/:id", RestoreAutoBackup) r.POST("/auto_backup/test_s3", TestS3Connection) ``` Those handlers create or modify backup jobs, register cron jobs, test attacker-supplied S3 settings, and store S3 credentials: ```go "s3_access_key_id": "omitempty", "s3_secret_access_key": "omitempty", ... if ctx.Model.Enabled { if err := cron.AddAutoBackupJob(ctx.Model.ID, ctx.Model.CronExpression); err != nil { ``` The model stores S3 secret material: ```go S3AccessKeyID string `json:"s3_access_key_id" gorm:"comment:S3 access key ID;serializer:json[aes]"` S3SecretAccessKey string `json:"s3_secret_access_key" gorm:"comment:S3 secret access key;serializer:json[aes]"` ``` ### Certificate, ACME, and DNS credential management `api/certificate/router.go` registers credential and certificate mutations without `RequireSecureSession()`: ```go r.POST("dns_credentials", AddDnsCredential) r.POST("dns_credentials/:id", EditDnsCredential) r.DELETE("dns_credentials/:id", DeleteDnsCredential) r.POST("certs", AddCert) r.POST("certs/:id", ModifyCert) r.DELETE("certs/:id", RemoveCert) r.PUT("cert_sync", SyncCertificate) r.POST("acme_users", CreateAcmeUser) r.POST("acme_users/:id", ModifyAcmeUser) r.POST("acme_users/:id/register", RegisterAcmeUser) r.DELETE("acme_users/:id", DestroyAcmeUser) ``` The DNS credential handlers persist provider configuration: ```go dnsCredential := model.DnsCredential{ Name: json.Name, Config: &json.Config, Provider: json.Provider, ProviderCode: providerCode, } ... err := d.Create(&dnsCredential) ``` The model serializes that configuration with AES: ```go Config *dns.Config `json:"config,omitempty" gorm:"serializer:json[aes]"` ``` ### DNS domains, records, and DDNS `api/dns/router.go` registers DNS domain, record, and DDNS mutations without secure-session middleware: ```go group.POST("/domains", CreateDomain) group.POST("/domains/:id", UpdateDomain) group.DELETE("/domains/:id", DeleteDomain) group.POST("/domains/:id/records", CreateRecord) group.PUT("/domains/:id/records/:record_id", UpdateRecord) group.DELETE("/domains/:id/records/:record_id", DeleteRecord) group.PUT("/domains/:id/ddns", UpdateDDNSConfig) group.DELETE("/domains/:id/ddns", DeleteDDNSConfig) ``` Those handlers call the DNS provider service and modify DDNS jobs: ```go record, err := svc.CreateRecord(c.Request.Context(), domainID, toRecordInput(payload)) ... record, err := svc.UpdateRecord(c.Request.Context(), domainID, recordID, toRecordInput(payload)) ... if err := cron.AddOrUpdateDDNSJob(domainID, cfg.IntervalSeconds); err != nil { ``` ### Nginx control and configuration-side effects `api/nginx/router.go` registers several server/Nginx mutation endpoints without `RequireSecureSession()`: ```go r.POST("nginx/reload", Reload) r.POST("nginx/restart", Restart) r.POST("nginx/stub_status", ToggleStubStatus) r.POST("nginx/performance", UpdatePerformanceSettings) r.POST("nginx/modules/refresh", RefreshModulesCache) ``` The handlers perform control actions or persist Nginx-related changes: ```go func Reload(c *gin.Context) { nginx.Control(nginx.Reload).Resp(c) } func Restart(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "ok", }) go nginx.Restart() } ``` ```go if json.Enable { err = performance.EnableStubStatus() } else { err = performance.DisableStubStatus() } ... reloadOutput, err := nginx.Reload() ``` ```go err := performance.UpdatePerfOpt(&perfOpt) ``` ### Site and stream advanced/batch mutations `api/sites/router.go` leaves these site mutations before the secure-session group: ```go r.PUT("sites", BatchUpdateSites) r.POST("sites/:name/advance", DomainEditByAdvancedMode) r.POST("auto_cert/:name", AddDomainToAutoCert) r.DELETE("auto_cert/:name", RemoveDomainFromAutoCert) r.POST("site_navigation/order", UpdateSiteOrder) r.POST("site_navigation/health_check/:id", UpdateHealthCheck) ``` `DomainEditByAdvancedMode()` persists the site's `Advanced` mode: ```go _, err = s.Where(s.Path.Eq(path)).Update(s.Advanced, json.Advanced) ``` `AddDomainToAutoCert()` writes certificate automation settings including DNS credential ID, domains, challenge method, and ACME user: ```go err = certModel.Updates(&model.Cert{ Name: name, Domains: json.Domains, AutoCert: model.AutoCertEnabled, DnsCredentialID: json.DnsCredentialID, ChallengeMethod: json.ChallengeMethod, KeyType: helper.GetKeyType(json.KeyType), ACMEUserID: json.ACMEUserID, }) ``` `api/streams/router.go` protects normal stream save/delete operations, but leaves batch update and advanced-mode outside: ```go r.PUT("streams", BatchUpdateStreams) ... r.POST("streams/:name/advance", AdvancedEdit) ``` `AdvancedEdit()` persists the stream's `Advanced` mode: ```go _, err = s.Where(s.Path.Eq(path)).Update(s.Advanced, json.Advanced) ``` ## Safe local proof I did not access any external Nginx UI deployment. The following proof only inspects the checked-out source tree and confirms that: - `RequireSecureSession()` is a real OTP-gated 10-minute step-up mechanism; - high-risk control routes are intentionally protected elsewhere; - the affected route groups are authenticated but outside `RequireSecureSession()`; - the affected handlers perform persistent or server-control side effects. Run: ```bash python3 reports/nginx-ui-missing-secure-session-step-up-poc.py ``` Observed output: ```json { "all_affected_groups_present": true, "all_controls_present": true, "all_side_effects_present": true, "commit": "4948fba8744e82c3d7003f68ee164e9f49421066", "result": "VULNERABLE_SOURCE_CONFIRMED", "target": "0xJacky/nginx-ui" } ``` The full local proof output is available in my workspace as `tmp/nginx-ui-missing-secure-session-step-up-poc-output.txt`. ## HTTP reproduction outline This is a local-only reproduction outline for an isolated Nginx UI test instance, consistent with the project's security policy: 1. Create a test user with OTP enabled. 2. Log in as that user and keep only the normal authenticated session/JWT. 3. Do not obtain or send a fresh `X-Secure-Session-ID`. 4. Verify the expected protected control behavior: ```http POST /api/config Authorization: <otp-user-session> Content-Type: application/json { "name": "example.conf", "content": "server { listen 80; }" } ``` Expected result: `401 Unauthorized` because config edits require `RequireSecureSession()`. 5. Attempt an affected operation with the same OTP-enabled session and no `X-Secure-Session-ID`, for example: ```http POST /api/dns_credentials Authorization: <otp-user-session> Content-Type: application/json { "name": "attacker-controlled-test", "provider": "cloudflare", "provider_code": "cloudflare", "token": "test-token" } ``` Expected secure behavior: `401 Unauthorized`, matching the secure-session requirement for other sensitive configuration changes. Actual behavior from source review: the request reaches `AddDnsCredential()` because the DNS credential router is under `AuthRequired()` but does not apply `RequireSecureSession()`. The same missing step-up applies to the other route groups listed above. ## Impact If an attacker obtains the authenticated web session/JWT for a 2FA-enabled Nginx UI user, the secure-session boundary should limit what they can do until they also pass a fresh OTP/passkey challenge. The current route map leaves major administrative actions outside that boundary. Depending on the victim user's privileges and deployment configuration, this can allow the attacker to: - add, edit, or remove DNS provider credentials; - alter certificate and ACME automation; - create, modify, delete, or restore automatic backup jobs; - configure S3 backup destinations and trigger S3 connection tests; - alter DNS domains, records, or DDNS schedules through configured providers; - restart or reload Nginx; - enable/disable stub-status and change performance settings; - switch sites/streams into advanced mode or change site automation bindings. This undermines the value of the secure-session step-up for OTP-enabled accounts. I am not claiming pre-authentication access or remote code execution from this issue. The issue is missing step-up authorization for sensitive authenticated mutations. ## Suggested fix Recommended mitigation: 1. Apply `middleware.RequireSecureSession()` to all high-impact authenticated mutation routes that alter server, Nginx, certificate, DNS, backup, credential, or automation state. 2. At minimum, protect: - auto-backup create/modify/delete/restore/test-S3; - DNS credential create/edit/delete; - certificate and ACME user mutations; - DNS domain, record, and DDNS mutations; - Nginx reload/restart/stub-status/performance/module-cache mutation routes; - site/stream advanced-mode, batch, auto-cert, navigation order, and health-check mutations. 3. Add regression tests mirroring the existing config/site/stream security tests: - OTP-enabled user without `X-Secure-Session-ID` receives `401`; - OTP-enabled user with a valid secure-session ID succeeds; - non-OTP users retain existing behavior if that is the intended product policy. 4. Consider centralizing "sensitive mutation" routing to make future omissions harder.

CVE-2026-84315 Affected Github.com/0xJacky/Nginx UI Versions

1 component(s)
Fixed Versions:
1.9.10-0.20260703131214-23d1934099b0
Product / Package Ecosystem Affected Versions Fixed Version
github.com/0xJacky/Nginx-UI
Go
< 1.9.10-0.20260703131214-23d1934099b0
1.9.10-0.20260703131214-23d1934099b0

References

Frequently Asked Questions

What is CVE-2026-84315?
CVE-2026-84315 is a security vulnerability in Github.com/0xJacky/Nginx UI involving Nginx UI OTP secure-session step-up is missing on sensitive mutation routes under CWE-862: Missing Authorization.
Which Github.com/0xJacky/Nginx UI versions are affected?
Affected versions include releases prior to 1.9.10-0.20260703131214-23d1934099b0 across affected packages: github.com/0xJacky/Nginx-UI.
What version fixes CVE-2026-84315?
Upgrading to 1.9.10-0.20260703131214-23d1934099b0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/0xJacky/Nginx-UI affected?
Yes, github.com/0xJacky/Nginx-UI is affected in releases prior to 1.9.10-0.20260703131214-23d1934099b0. Upgrade your dependency to 1.9.10-0.20260703131214-23d1934099b0 or later.
What is the CVSS score of CVE-2026-84315?
CVE-2026-84315 has a CVSS v3.1 base score of 8.3 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:H.
What CWE is associated with CVE-2026-84315?
CVE-2026-84315 is classified under CWE-862: Missing Authorization (The product does not perform an authorization check when an actor attempts to access a resource or perform an action.).
How can I check Go dependencies for CVE-2026-84315?
Run the dependency tree command: 'inspect dependencies for github.com/0xJacky/Nginx-UI'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-5v7c-xpfp-p65m",
  "modified" : "2026-10-15T20:12:42Z",
  "published" : "2026-10-15T20:12:42Z",
  "aliases" : [ "CVE-2026-84315" ],
  "summary" : "Nginx UI OTP secure-session step-up is missing on sensitive mutation routes",
  "details" : "Reporter: Jagadesh Achanta\n\nTarget: `0xJacky/nginx-ui`\n\nAffected version / commit tested:\n- App version: `2.3.10`\n- Commit: `4948fba8744e82c3d7003f68ee164e9f49421066`\n- Disclosure channel: GitHub Private Vulnerability Reporting\n- Credential reference: `cred:github-jagstack-gh-cli`\n\n## AI assistance disclosure\n\nThis report was prepared by Jagadesh Achanta with assistance from OpenAI/Codex for public source review, local evidence collection, and report drafting.\n\n## Summary\n\nNginx UI has a short-lived OTP/passkey \"secure session\" step-up boundary for users with 2FA enabled. `RequireSecureSession()` rejects an OTP-enabled user's request unless the request includes a valid `X-Secure-Session-ID` issued after a fresh OTP/passkey verification.\n\nThe project already applies this step-up middleware to sensitive operations such as:\n\n- Nginx config create/edit/rename/delete;\n- site save/delete/enable/disable/rename/duplicate;\n- stream save/delete/enable/disable/rename/duplicate;\n- settings save and protected secret reads;\n- backup restore.\n\nHowever, several similarly sensitive authenticated mutation routes remain outside `RequireSecureSession()`. An attacker who obtains an authenticated JWT/session for a 2FA-enabled user, but does not have the victim's fresh OTP/passkey secure-session token, can still perform those operations.\n\nThe affected route groups include:\n\n- automatic backup create/modify/delete/restore/test-S3 operations;\n- DNS credential create/edit/delete operations;\n- certificate and ACME user create/modify/delete/register operations;\n- DNS domain, record, and DDNS create/update/delete operations;\n- Nginx reload/restart, stub-status toggle, performance-setting update, and module-cache refresh operations;\n- site/stream advanced-mode toggles and batch updates;\n- auto-certificate binding/unbinding for sites;\n- site navigation order and health-check updates.\n\nThis report is distinct from my earlier Nginx UI report about auto-backup path traversal. This report is about missing 2FA step-up authorization on sensitive authenticated mutation routes.\n\n## Why this is a security boundary\n\n`internal/middleware/secure_session.go` only requires a secure-session token for users with OTP enabled. For OTP users, it reads `X-Secure-Session-ID` from the header or query string and aborts with `401 Unauthorized` if the secure session is missing or invalid:\n\n```go\nif !cUser.EnabledOTP() {\n    c.Next()\n    return\n}\nssid := c.GetHeader(\"X-Secure-Session-ID\")\nif ssid == \"\" {\n    ssid = c.Query(\"X-Secure-Session-ID\")\n}\nif ssid == \"\" {\n    c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{\n        \"message\": \"Secure Session ID is empty\",\n    })\n    return\n}\n\nif user.VerifySecureSessionID(ssid, cUser.ID) {\n    c.Next()\n    return\n}\n```\n\n`internal/user/otp.go` issues the secure-session ID for only 10 minutes:\n\n```go\nfunc SetSecureSessionID(userId uint64) (sessionId string) {\n    sessionId = uuid.NewString()\n    cache.Set(secureSessionIDCacheKey(sessionId), userId, 10*time.Minute)\n    return\n}\n```\n\nThe UI/server already rely on this boundary for sensitive actions. For example, `api/config/router.go` wraps config mutations with `RequireSecureSession()`:\n\n```go\no := r.Group(\"\", middleware.RequireSecureSession())\n{\n    o.POST(\"configs\", AddConfig)\n    o.POST(\"config\", EditConfig)\n    o.POST(\"config_mkdir\", Mkdir)\n    o.POST(\"config_rename\", Rename)\n    o.POST(\"config_delete\", DeleteConfig)\n}\n```\n\n`api/sites/router.go` protects normal site mutations:\n\n```go\no := r.Group(\"\", middleware.RequireSecureSession())\n{\n    o.POST(\"sites/:name/rename\", RenameSite)\n    o.POST(\"sites/:name/enable\", EnableSite)\n    o.POST(\"sites/:name/disable\", DisableSite)\n    o.POST(\"sites/:name\", SaveSite)\n    o.DELETE(\"sites/:name\", DeleteSite)\n    o.POST(\"sites/:name/duplicate\", DuplicateSite)\n    o.POST(\"sites/:name/maintenance\", EnableMaintenanceSite)\n}\n```\n\n`api/streams/router.go` protects normal stream mutations, and `api/settings/router.go` protects secret reads/settings saves:\n\n```go\no := r.Group(\"\", middleware.RequireSecureSession())\n{\n    o.POST(\"streams/:name\", SaveStream)\n    o.POST(\"streams/:name/rename\", RenameStream)\n    o.POST(\"streams/:name/enable\", EnableStream)\n    o.POST(\"streams/:name/disable\", DisableStream)\n    o.DELETE(\"streams/:name\", DeleteStream)\n    o.POST(\"streams/:name/duplicate\", Duplicate)\n}\n```\n\n```go\nr.GET(\"settings/protected\", middleware.RequireSecureSession(), GetProtectedSetting)\nr.POST(\"settings\", middleware.RequireSecureSession(), SaveSettings)\n```\n\nThe repository's own tests also verify this expected behavior. `api/config/security_test.go`, `api/sites/security_test.go`, and `api/streams/security_test.go` each assert that OTP-enabled users receive `401 Unauthorized` when protected mutations are attempted without a secure session.\n\n## Affected routes outside the step-up boundary\n\nThe affected routes are under the authenticated private router:\n\n```go\ng := root.Group(\"/\", middleware.AuthRequired(), middleware.Proxy())\n```\n\nbut they do not add `middleware.RequireSecureSession()`.\n\n### Automatic backup management\n\n`api/backup/router.go` protects manual restore:\n\n```go\nr.POST(\"/restore\", middleware.AuthRequired(), middleware.RequireSecureSession(), middleware.EncryptedForm(), RestoreBackup)\n```\n\nbut the automatic backup management router has no secure-session middleware:\n\n```go\nr.POST(\"/auto_backup\", CreateAutoBackup)\nr.POST(\"/auto_backup/:id\", ModifyAutoBackup)\nr.DELETE(\"/auto_backup/:id\", DestroyAutoBackup)\nr.PATCH(\"/auto_backup/:id\", RestoreAutoBackup)\nr.POST(\"/auto_backup/test_s3\", TestS3Connection)\n```\n\nThose handlers create or modify backup jobs, register cron jobs, test attacker-supplied S3 settings, and store S3 credentials:\n\n```go\n\"s3_access_key_id\":     \"omitempty\",\n\"s3_secret_access_key\": \"omitempty\",\n...\nif ctx.Model.Enabled {\n    if err := cron.AddAutoBackupJob(ctx.Model.ID, ctx.Model.CronExpression); err != nil {\n```\n\nThe model stores S3 secret material:\n\n```go\nS3AccessKeyID     string `json:\"s3_access_key_id\" gorm:\"comment:S3 access key ID;serializer:json[aes]\"`\nS3SecretAccessKey string `json:\"s3_secret_access_key\" gorm:\"comment:S3 secret access key;serializer:json[aes]\"`\n```\n\n### Certificate, ACME, and DNS credential management\n\n`api/certificate/router.go` registers credential and certificate mutations without `RequireSecureSession()`:\n\n```go\nr.POST(\"dns_credentials\", AddDnsCredential)\nr.POST(\"dns_credentials/:id\", EditDnsCredential)\nr.DELETE(\"dns_credentials/:id\", DeleteDnsCredential)\n\nr.POST(\"certs\", AddCert)\nr.POST(\"certs/:id\", ModifyCert)\nr.DELETE(\"certs/:id\", RemoveCert)\nr.PUT(\"cert_sync\", SyncCertificate)\n\nr.POST(\"acme_users\", CreateAcmeUser)\nr.POST(\"acme_users/:id\", ModifyAcmeUser)\nr.POST(\"acme_users/:id/register\", RegisterAcmeUser)\nr.DELETE(\"acme_users/:id\", DestroyAcmeUser)\n```\n\nThe DNS credential handlers persist provider configuration:\n\n```go\ndnsCredential := model.DnsCredential{\n    Name:         json.Name,\n    Config:       &json.Config,\n    Provider:     json.Provider,\n    ProviderCode: providerCode,\n}\n...\nerr := d.Create(&dnsCredential)\n```\n\nThe model serializes that configuration with AES:\n\n```go\nConfig *dns.Config `json:\"config,omitempty\" gorm:\"serializer:json[aes]\"`\n```\n\n### DNS domains, records, and DDNS\n\n`api/dns/router.go` registers DNS domain, record, and DDNS mutations without secure-session middleware:\n\n```go\ngroup.POST(\"/domains\", CreateDomain)\ngroup.POST(\"/domains/:id\", UpdateDomain)\ngroup.DELETE(\"/domains/:id\", DeleteDomain)\ngroup.POST(\"/domains/:id/records\", CreateRecord)\ngroup.PUT(\"/domains/:id/records/:record_id\", UpdateRecord)\ngroup.DELETE(\"/domains/:id/records/:record_id\", DeleteRecord)\ngroup.PUT(\"/domains/:id/ddns\", UpdateDDNSConfig)\ngroup.DELETE(\"/domains/:id/ddns\", DeleteDDNSConfig)\n```\n\nThose handlers call the DNS provider service and modify DDNS jobs:\n\n```go\nrecord, err := svc.CreateRecord(c.Request.Context(), domainID, toRecordInput(payload))\n...\nrecord, err := svc.UpdateRecord(c.Request.Context(), domainID, recordID, toRecordInput(payload))\n...\nif err := cron.AddOrUpdateDDNSJob(domainID, cfg.IntervalSeconds); err != nil {\n```\n\n### Nginx control and configuration-side effects\n\n`api/nginx/router.go` registers several server/Nginx mutation endpoints without `RequireSecureSession()`:\n\n```go\nr.POST(\"nginx/reload\", Reload)\nr.POST(\"nginx/restart\", Restart)\nr.POST(\"nginx/stub_status\", ToggleStubStatus)\nr.POST(\"nginx/performance\", UpdatePerformanceSettings)\nr.POST(\"nginx/modules/refresh\", RefreshModulesCache)\n```\n\nThe handlers perform control actions or persist Nginx-related changes:\n\n```go\nfunc Reload(c *gin.Context) {\n    nginx.Control(nginx.Reload).Resp(c)\n}\n\nfunc Restart(c *gin.Context) {\n    c.JSON(http.StatusOK, gin.H{\n        \"message\": \"ok\",\n    })\n    go nginx.Restart()\n}\n```\n\n```go\nif json.Enable {\n    err = performance.EnableStubStatus()\n} else {\n    err = performance.DisableStubStatus()\n}\n...\nreloadOutput, err := nginx.Reload()\n```\n\n```go\nerr := performance.UpdatePerfOpt(&perfOpt)\n```\n\n### Site and stream advanced/batch mutations\n\n`api/sites/router.go` leaves these site mutations before the secure-session group:\n\n```go\nr.PUT(\"sites\", BatchUpdateSites)\nr.POST(\"sites/:name/advance\", DomainEditByAdvancedMode)\nr.POST(\"auto_cert/:name\", AddDomainToAutoCert)\nr.DELETE(\"auto_cert/:name\", RemoveDomainFromAutoCert)\nr.POST(\"site_navigation/order\", UpdateSiteOrder)\nr.POST(\"site_navigation/health_check/:id\", UpdateHealthCheck)\n```\n\n`DomainEditByAdvancedMode()` persists the site's `Advanced` mode:\n\n```go\n_, err = s.Where(s.Path.Eq(path)).Update(s.Advanced, json.Advanced)\n```\n\n`AddDomainToAutoCert()` writes certificate automation settings including DNS credential ID, domains, challenge method, and ACME user:\n\n```go\nerr = certModel.Updates(&model.Cert{\n    Name:            name,\n    Domains:         json.Domains,\n    AutoCert:        model.AutoCertEnabled,\n    DnsCredentialID: json.DnsCredentialID,\n    ChallengeMethod: json.ChallengeMethod,\n    KeyType:         helper.GetKeyType(json.KeyType),\n    ACMEUserID:      json.ACMEUserID,\n})\n```\n\n`api/streams/router.go` protects normal stream save/delete operations, but leaves batch update and advanced-mode outside:\n\n```go\nr.PUT(\"streams\", BatchUpdateStreams)\n...\nr.POST(\"streams/:name/advance\", AdvancedEdit)\n```\n\n`AdvancedEdit()` persists the stream's `Advanced` mode:\n\n```go\n_, err = s.Where(s.Path.Eq(path)).Update(s.Advanced, json.Advanced)\n```\n\n## Safe local proof\n\nI did not access any external Nginx UI deployment. The following proof only inspects the checked-out source tree and confirms that:\n\n- `RequireSecureSession()` is a real OTP-gated 10-minute step-up mechanism;\n- high-risk control routes are intentionally protected elsewhere;\n- the affected route groups are authenticated but outside `RequireSecureSession()`;\n- the affected handlers perform persistent or server-control side effects.\n\nRun:\n\n```bash\npython3 reports/nginx-ui-missing-secure-session-step-up-poc.py\n```\n\nObserved output:\n\n```json\n{\n  \"all_affected_groups_present\": true,\n  \"all_controls_present\": true,\n  \"all_side_effects_present\": true,\n  \"commit\": \"4948fba8744e82c3d7003f68ee164e9f49421066\",\n  \"result\": \"VULNERABLE_SOURCE_CONFIRMED\",\n  \"target\": \"0xJacky/nginx-ui\"\n}\n```\n\nThe full local proof output is available in my workspace as `tmp/nginx-ui-missing-secure-session-step-up-poc-output.txt`.\n\n## HTTP reproduction outline\n\nThis is a local-only reproduction outline for an isolated Nginx UI test instance, consistent with the project's security policy:\n\n1. Create a test user with OTP enabled.\n2. Log in as that user and keep only the normal authenticated session/JWT.\n3. Do not obtain or send a fresh `X-Secure-Session-ID`.\n4. Verify the expected protected control behavior:\n\n```http\nPOST /api/config\nAuthorization: <otp-user-session>\nContent-Type: application/json\n\n{ \"name\": \"example.conf\", \"content\": \"server { listen 80; }\" }\n```\n\nExpected result: `401 Unauthorized` because config edits require `RequireSecureSession()`.\n\n5. Attempt an affected operation with the same OTP-enabled session and no `X-Secure-Session-ID`, for example:\n\n```http\nPOST /api/dns_credentials\nAuthorization: <otp-user-session>\nContent-Type: application/json\n\n{\n  \"name\": \"attacker-controlled-test\",\n  \"provider\": \"cloudflare\",\n  \"provider_code\": \"cloudflare\",\n  \"token\": \"test-token\"\n}\n```\n\nExpected secure behavior: `401 Unauthorized`, matching the secure-session requirement for other sensitive configuration changes.\n\nActual behavior from source review: the request reaches `AddDnsCredential()` because the DNS credential router is under `AuthRequired()` but does not apply `RequireSecureSession()`.\n\nThe same missing step-up applies to the other route groups listed above.\n\n## Impact\n\nIf an attacker obtains the authenticated web session/JWT for a 2FA-enabled Nginx UI user, the secure-session boundary should limit what they can do until they also pass a fresh OTP/passkey challenge. The current route map leaves major administrative actions outside that boundary.\n\nDepending on the victim user's privileges and deployment configuration, this can allow the attacker to:\n\n- add, edit, or remove DNS provider credentials;\n- alter certificate and ACME automation;\n- create, modify, delete, or restore automatic backup jobs;\n- configure S3 backup destinations and trigger S3 connection tests;\n- alter DNS domains, records, or DDNS schedules through configured providers;\n- restart or reload Nginx;\n- enable/disable stub-status and change performance settings;\n- switch sites/streams into advanced mode or change site automation bindings.\n\nThis undermines the value of the secure-session step-up for OTP-enabled accounts. I am not claiming pre-authentication access or remote code execution from this issue. The issue is missing step-up authorization for sensitive authenticated mutations.\n\n## Suggested fix\n\nRecommended mitigation:\n\n1. Apply `middleware.RequireSecureSession()` to all high-impact authenticated mutation routes that alter server, Nginx, certificate, DNS, backup, credential, or automation state.\n2. At minimum, protect:\n   - auto-backup create/modify/delete/restore/test-S3;\n   - DNS credential create/edit/delete;\n   - certificate and ACME user mutations;\n   - DNS domain, record, and DDNS mutations;\n   - Nginx reload/restart/stub-status/performance/module-cache mutation routes;\n   - site/stream advanced-mode, batch, auto-cert, navigation order, and health-check mutations.\n3. Add regression tests mirroring the existing config/site/stream security tests:\n   - OTP-enabled user without `X-Secure-Session-ID` receives `401`;\n   - OTP-enabled user with a valid secure-session ID succeeds;\n   - non-OTP users retain existing behavior if that is the intended product policy.\n4. Consider centralizing \"sensitive mutation\" routing to make future omissions harder.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/0xJacky/Nginx-UI"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.9.10-0.20260703131214-23d1934099b0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/0xJacky/nginx-ui/security/advisories/GHSA-5v7c-xpfp-p65m"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-84315"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/0xJacky/nginx-ui/commit/23d1934099b05b2a01589b48129dc3d3159d91c2"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/0xJacky/nginx-ui"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/0xJacky/nginx-ui/releases/tag/v2.4.1"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-862" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-10-15T20:12:42Z",
    "nvd_published_at" : "2026-10-12T21:17:21Z"
  }
}