{"id":"CVE-2026-58422","aliases":["GHSA-g9g6-qhrc-p3qc"],"title":"Gitea: Improper authorization on OAuth sign-in callback silently re-enables administrator-disabled accounts","summary":"Gitea: Improper authorization on OAuth sign-in callback silently re-enables administrator-disabled accounts","severity":"high","cwe":["CWE-284"],"vendor":"gitea","product":"code.gitea.io/gitea","ecosystem":"go","affected":["code.gitea.io/gitea < 1.26.4"],"patched":["code.gitea.io/gitea 1.26.4"],"published":"2026-07-21","updated":"2026-07-21","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-g9g6-qhrc-p3qc","references":[{"url":"https://github.com/go-gitea/gitea/security/advisories/GHSA-g9g6-qhrc-p3qc"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-58422"},{"url":"https://github.com/go-gitea/gitea/pull/38009"},{"url":"https://github.com/go-gitea/gitea/commit/c43eb7c33a100ffc7b2367adf165f7085e0ccdc5"},{"url":"https://blog.gitea.com/release-of-1.26.3-and-1.26.4"},{"url":"https://github.com/go-gitea/gitea/releases/tag/v1.26.4"},{"url":"https://github.com/advisories/GHSA-g9g6-qhrc-p3qc"}],"tags":["ghsa","go"],"epss":0.00621,"epssPercentile":0.4754,"ingestedAt":"2026-07-21T21:54:47.320Z","slug":"CVE-2026-58422","body":"## Overview\n\n### Summary\n\nThe OAuth2 sign-in callback in Gitea 1.26.1 unconditionally re-enables a locally-disabled account whenever the user authenticates through a linked external identity provider, silently undoing any administrator-initiated `Disable Account` action and issuing a fresh authenticated session in the same response. Once a user has linked any external IdP, the administrator's disable toggle becomes non-binding — the user can re-enable themselves on demand by completing one OAuth callback, regaining full read and write access to their repositories, organizations, and access tokens.\n\n### Details\n\nImproper Authorization is present on the `code` and `state` parameters of the `/user/oauth2/{source-name}/callback` endpoint in Gitea version 1.26.1. The handler `routers/web/auth/oauth.go::handleOAuth2SignIn` reads the local user's `IsActive` flag at lines 350-352 and, when it is `false`, sets `opts.IsActive = optional.Some(true)` before calling `user_service.UpdateUser` and immediately establishing a session. The active-state precondition that the request middleware `routers/web/web.go::verifyAuthWithOptions` enforces for every other request is therefore evaluated against the freshly-flipped row on the next request, so a site administrator's \"disable user\" action is silently undone the next time the user authenticates through any linked external identity provider.\n\nThe root cause is that the callback path treats the local `IsActive` flag as stale bookkeeping to reconcile against the external identity, rather than as an authoritative administrative override. The local-credential sign-in path correctly renders `IsErrUserInactive` in the same condition; only the OAuth callback path flips the flag and proceeds.\n\n### PoC\n\n1. As a site administrator, configure an OAuth2 authentication source via `Site Administration -> Authentication Sources -> Add Authentication Source`. Any external provider works (a self-hosted Keycloak, an upstream Gitea acting as OIDC, GitHub, GitLab, Microsoft, Google).\n2. Have a normal user (`alice` in the example) sign in through that source at least once. The callback creates the row in `external_login_user` linking alice's local account to the IdP identity.\n3. As the site administrator, disable alice's account: `Site Administration -> User Accounts -> alice -> \"Disable Account\"`. Confirm the flag is set with the admin API.\n\n#### HTTP REQUEST\n\n```http\nGET /api/v1/admin/users/alice HTTP/1.1\nHost: <HOST>\nAuthorization: token <ADMIN_TOKEN>\nAccept: application/json\n```\n\n#### HTTP RESPONSE\n\n```http\nHTTP/1.1 200 OK\nContent-Type: application/json;charset=utf-8\n\n{\"id\":2,\"login\":\"alice\",\"active\":false,...}\n```\n\n4. Have alice click \"Sign in with <source-name>\" on the Gitea login page and complete the OAuth dance with the IdP. The callback handler runs, observes `IsActive=false`, sets `opts.IsActive=true`, calls `UpdateUser`, and issues a session on the same response. Alice lands on `/`.\n5. Re-read the same admin API endpoint and observe that `active` has flipped back to `true` without administrator action.\n\n#### HTTP REQUEST\n\n```http\nGET /api/v1/admin/users/alice HTTP/1.1\nHost: <HOST>\nAuthorization: token <ADMIN_TOKEN>\nAccept: application/json\n```\n\n#### HTTP RESPONSE\n\n```http\nHTTP/1.1 200 OK\nContent-Type: application/json;charset=utf-8\n\n{\"id\":2,\"login\":\"alice\",\"active\":true,...}\n```\n\n### Impact\n\nOnce a user has linked any external identity provider, the site administrator's \"disable user\" toggle becomes non-binding on that account: the user can re-enable themselves on demand by completing one OAuth callback, regaining full read and write access to their repositories, organizations, and tokens. In SSO-fronted Gitea deployments where the disable toggle is the documented incident-response action for compromised accounts or departed employees, this defeats the only short-term remediation available before the identity provider can revoke the underlying federated identity.\n\n## Affected packages\n\n- `code.gitea.io/gitea < 1.26.4`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `code.gitea.io/gitea 1.26.4`","depth":"twilight","depthScore":41,"depthScoreParts":{"impact":41.3,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}