GHSA-44xr-mrhh-qm78

Suggest an improvement
Source
https://github.com/advisories/GHSA-44xr-mrhh-qm78
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-44xr-mrhh-qm78/GHSA-44xr-mrhh-qm78.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-44xr-mrhh-qm78
Aliases
Published
2026-10-15T20:12:52Z
Modified
2026-10-15T20:30:07Z
Severity
  • 7.1 (High) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:N CVSS Calculator
Summary
Nginx UI: Casdoor SSO callback uses a constant `state` parameter (login CSRF)
Details

Summary

In the Casdoor SSO flow, the OAuth state parameter is hard-coded to settings.CasdoorSettings.Application — a value identical for every user and every login. The callback handler accepts whatever state the client sends without comparing it to anything stored in the user agent, enabling a login-CSRF: an attacker can force the victim's browser to authenticate as the attacker.

Details

api/user/casdoor.go:92-117 (GetCasdoorUri):

func GetCasdoorUri(c *gin.Context) {
    clientId := settings.CasdoorSettings.ClientId
    redirectUri := settings.CasdoorSettings.RedirectUri
    state := settings.CasdoorSettings.Application      // ← constant
    ...
    c.JSON(http.StatusOK, gin.H{
        "uri": fmt.Sprintf(
            "%s/login/oauth/authorize?client_id=%s&response_type=code&redirect_uri=%s&state=%s&scope=read",
            endpoint, clientId, encodedRedirectUri, state),
    })
}

api/user/casdoor.go:24-90 (CasdoorCallback):

type CasdoorLoginUser struct {
    Code  string `json:"code" binding:"required,max=255"`
    State string `json:"state" binding:"required,max=255"`
}

func CasdoorCallback(c *gin.Context) {
    var loginUser CasdoorLoginUser
    if !cosy.BindAndValid(c, &loginUser) { return }
    ...
    // state is NEVER compared to a per-session value
    token, err := casdoorsdk.GetOAuthToken(loginUser.Code, loginUser.State)
    ...
    u, err := user.GetUser(claims.Name)        // username from Casdoor's JWT
    ...
    userToken, err := user.GenerateJWT(u)       // JWT minted for whatever user came back
}

Compare with the OIDC implementation in api/user/oidc.go:35-48, which generates a random state, stores it in an oidc_state cookie, and validates it on the callback. Casdoor has no such check.

Proof of Concept

  1. Attacker initiates a Casdoor authorisation flow on their own browser, obtaining an authorisation code for their Casdoor account.

  2. Attacker hosts a page that auto-submits the captured code to the victim's nginx-ui:

    <html><body><form action="https://victim.example/api/casdoor_callback" method="POST"
                      enctype="text/plain" id="x">
      <input name='{"code":"<attacker_code>","state":"<App>","junk":"' value='"}'>
    </form>
    <script>document.getElementById('x').submit()</script>
    </body></html>
    
  3. Server exchanges the attacker's code, looks up the username from claims.Name in the local user table, and mints a JWT for that row. The callback also sets the _nginx_ui_secure_session cookie via middleware.EnsureSecureSessionCookie(c) (casdoor.go:84) — meaning the victim is now silently authenticated as the attacker.

  4. Victim, believing they are using their own account, configures sites / TLS keys; the attacker reads them.

Impact

Login CSRF — the victim is forced to authenticate as the attacker, leading to data exfiltration of anything the victim subsequently uploads or configures. Particularly dangerous when nginx-ui is used by an organisation where Casdoor maps internal identities — the attacker can attribute their changes to the victim.

Patches

Mirror the OIDC implementation:

+ import (
+    "crypto/rand"
+    "crypto/subtle"
+    "encoding/hex"
+    ...
+ )

 func GetCasdoorUri(c *gin.Context) {
     ...
-    state := settings.CasdoorSettings.Application
+    b := make([]byte, 16)
+    if _, err := rand.Read(b); err != nil {
+        cosy.ErrHandler(c, err); return
+    }
+    state := hex.EncodeToString(b)
+    c.SetSameSite(http.SameSiteLaxMode)
+    c.SetCookie("casdoor_state", state, 600, "/", "",
+                cSettings.ServerSettings.EnableHTTPS, true)
     ...
 }

 func CasdoorCallback(c *gin.Context) {
+    expected, err := c.Cookie("casdoor_state")
+    if err != nil {
+        c.AbortWithStatusJSON(http.StatusBadRequest,
+            gin.H{"message":"missing state cookie"}); return
+    }
     var loginUser CasdoorLoginUser
     if !cosy.BindAndValid(c, &loginUser) { return }
+    if subtle.ConstantTimeCompare([]byte(expected), []byte(loginUser.State)) != 1 {
+        c.AbortWithStatusJSON(http.StatusBadRequest,
+            gin.H{"message":"invalid state"}); return
+    }
     ...
 }

Workarounds

Disable Casdoor SSO until patched. The OIDC integration in api/user/oidc.go is correctly implemented and can be used as a substitute.

References

Credits

Vishal Shukla (@shukla304) and Sechub.dev AI Agent.

Database specific
{
    "cwe_ids":  [
        "CWE-287",
        "CWE-352"
    ],
    "github_reviewed":  true,
    "github_reviewed_at":  "2026-10-15T20:12:52Z",
    "nvd_published_at":  "2026-10-12T22:17:05Z",
    "severity":  "HIGH"
}
References

Affected packages

Go / Nginx-UI

Package

Name
Nginx-UI
View open source insights on deps.dev
Purl
pkg:golang/Nginx-UI

Affected ranges

Type
SEMVER
Events
Introduced
0 Unknown introduced version / All previous versions are affected

Database specific

last_known_affected_version_range
"< 1.9.10-0.20260703131214-23d1934099b0"
source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-44xr-mrhh-qm78/GHSA-44xr-mrhh-qm78.json"