GHSA-44XR-MRHH-QM78

Vulnerability from github – Published: 2026-10-15 20:12 – Updated: 2026-10-15 20:12
VLAI
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
document.getElementById('x').submit()

```

  1. 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.
  2. 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.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c 1.9.10-0.20260703131214-23d1934099b0"
      },
      "package": {
        "ecosystem": "Go",
        "name": "Nginx-UI"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-84312"
  ],
  "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"
  },
  "details": "## Summary\n\nIn the Casdoor SSO flow, the OAuth `state` parameter is hard-coded to `settings.CasdoorSettings.Application` \u2014 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\u0027s browser to authenticate as the *attacker*.\n\n## Details\n\n`api/user/casdoor.go:92-117` (`GetCasdoorUri`):\n\n```go\nfunc GetCasdoorUri(c *gin.Context) {\n    clientId := settings.CasdoorSettings.ClientId\n    redirectUri := settings.CasdoorSettings.RedirectUri\n    state := settings.CasdoorSettings.Application      // \u2190 constant\n    ...\n    c.JSON(http.StatusOK, gin.H{\n        \"uri\": fmt.Sprintf(\n            \"%s/login/oauth/authorize?client_id=%s\u0026response_type=code\u0026redirect_uri=%s\u0026state=%s\u0026scope=read\",\n            endpoint, clientId, encodedRedirectUri, state),\n    })\n}\n```\n\n`api/user/casdoor.go:24-90` (`CasdoorCallback`):\n\n```go\ntype CasdoorLoginUser struct {\n    Code  string `json:\"code\" binding:\"required,max=255\"`\n    State string `json:\"state\" binding:\"required,max=255\"`\n}\n\nfunc CasdoorCallback(c *gin.Context) {\n    var loginUser CasdoorLoginUser\n    if !cosy.BindAndValid(c, \u0026loginUser) { return }\n    ...\n    // state is NEVER compared to a per-session value\n    token, err := casdoorsdk.GetOAuthToken(loginUser.Code, loginUser.State)\n    ...\n    u, err := user.GetUser(claims.Name)        // username from Casdoor\u0027s JWT\n    ...\n    userToken, err := user.GenerateJWT(u)       // JWT minted for whatever user came back\n}\n```\n\nCompare 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.\n\n## Proof of Concept\n\n1. Attacker initiates a Casdoor authorisation flow on their own browser, obtaining an authorisation `code` for *their* Casdoor account.\n2. Attacker hosts a page that auto-submits the captured `code` to the victim\u0027s nginx-ui:\n\n   ```html\n   \u003chtml\u003e\u003cbody\u003e\u003cform action=\"https://victim.example/api/casdoor_callback\" method=\"POST\"\n                     enctype=\"text/plain\" id=\"x\"\u003e\n     \u003cinput name=\u0027{\"code\":\"\u003cattacker_code\u003e\",\"state\":\"\u003cApp\u003e\",\"junk\":\"\u0027 value=\u0027\"}\u0027\u003e\n   \u003c/form\u003e\n   \u003cscript\u003edocument.getElementById(\u0027x\u0027).submit()\u003c/script\u003e\n   \u003c/body\u003e\u003c/html\u003e\n   ```\n\n3. Server exchanges the attacker\u0027s `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`) \u2014 meaning the victim is now silently authenticated as the attacker.\n4. Victim, believing they are using their own account, configures sites / TLS keys; the attacker reads them.\n\n## Impact\n\nLogin CSRF \u2014 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 \u2014 the attacker can attribute their changes to the victim.\n\n## Patches\n\nMirror the OIDC implementation:\n\n```diff\n+ import (\n+    \"crypto/rand\"\n+    \"crypto/subtle\"\n+    \"encoding/hex\"\n+    ...\n+ )\n\n func GetCasdoorUri(c *gin.Context) {\n     ...\n-    state := settings.CasdoorSettings.Application\n+    b := make([]byte, 16)\n+    if _, err := rand.Read(b); err != nil {\n+        cosy.ErrHandler(c, err); return\n+    }\n+    state := hex.EncodeToString(b)\n+    c.SetSameSite(http.SameSiteLaxMode)\n+    c.SetCookie(\"casdoor_state\", state, 600, \"/\", \"\",\n+                cSettings.ServerSettings.EnableHTTPS, true)\n     ...\n }\n\n func CasdoorCallback(c *gin.Context) {\n+    expected, err := c.Cookie(\"casdoor_state\")\n+    if err != nil {\n+        c.AbortWithStatusJSON(http.StatusBadRequest,\n+            gin.H{\"message\":\"missing state cookie\"}); return\n+    }\n     var loginUser CasdoorLoginUser\n     if !cosy.BindAndValid(c, \u0026loginUser) { return }\n+    if subtle.ConstantTimeCompare([]byte(expected), []byte(loginUser.State)) != 1 {\n+        c.AbortWithStatusJSON(http.StatusBadRequest,\n+            gin.H{\"message\":\"invalid state\"}); return\n+    }\n     ...\n }\n```\n\n## Workarounds\n\nDisable Casdoor SSO until patched. The OIDC integration in `api/user/oidc.go` is correctly implemented and can be used as a substitute.\n\n## References\n\n* [`api/user/casdoor.go:92-117`](https://github.com/0xJacky/nginx-ui/blob/dev/api/user/casdoor.go#L92-L117)\n* [`api/user/casdoor.go:24-90`](https://github.com/0xJacky/nginx-ui/blob/dev/api/user/casdoor.go#L24-L90)\n* [`api/user/oidc.go:35-48`](https://github.com/0xJacky/nginx-ui/blob/dev/api/user/oidc.go#L35-L48) (correct implementation for comparison)\n\n## Credits\n\nVishal Shukla ([@shukla304](https://github.com/shukla304)) and Sechub.dev AI Agent.",
  "id": "GHSA-44xr-mrhh-qm78",
  "modified": "2026-10-15T20:12:52Z",
  "published": "2026-10-15T20:12:52Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/0xJacky/nginx-ui/security/advisories/GHSA-44xr-mrhh-qm78"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-84312"
    },
    {
      "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"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Nginx UI: Casdoor SSO callback uses a constant `state` parameter (login CSRF)"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    Related by attack behaviour

    Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.