GHSA-PWX3-QCGW-VH7H
Vulnerability from github – Published: 2026-06-23 00:02 – Updated: 2026-07-21 13:36Summary
In Gogs 0.14.1, organization team member management can be performed via GET requests without CSRF protection. If a victim who is an organization owner is logged in and is tricked into visiting a crafted link, an attacker-controlled user can be added to the Owners team. As a result, the attacker gains organization owner–equivalent privileges.
Description
When a victim is logged in as an organization owner, team member management endpoints are exposed via routes reachable by GET requests, allowing state-changing operations without a CSRF token.
Team action route allows GET
internal/cmd/web.go:390
m.Route("/teams/:team/action/:action", "GET,POST", org.TeamsAction)
CSRF validation is applied only to POST requests
Because the global CSRF check is limited to POST requests, state-changing operations reached via GET bypass CSRF protection entirely.
internal/context/auth.go:56-61
if !options.SignOutRequired && !options.DisableCSRF &&
c.Req.Method == "POST" && !isAPIPath(c.Req.URL.Path) {
csrf.Validate(c.Context, c.csrf)
if c.Written() {
return
}
}
TeamsAction performs state changes regardless of HTTP method
TeamsAction does not branch on the HTTP method. Instead, it performs state-changing operations (such as adding or removing members) based solely on query parameters (uid, uname) and the :action path parameter.
Since the route explicitly allows GET, the add action can be executed via GET.
internal/route/org/teams.go:38-83
func TeamsAction(c *context.Context) {
uid := com.StrTo(c.Query("uid")).MustInt64()
if uid == 0 {
c.Redirect(c.Org.OrgLink + "/teams")
return
}
page := c.Query("page")
var err error
switch c.Params(":action") {
case "add":
if !c.Org.IsOwner {
c.NotFound()
return
}
uname := c.Query("uname")
var u *database.User
u, err = database.Handle.Users().GetByUsername(c.Req.Context(), uname)
// ...
err = c.Org.Team.AddMember(u.ID)
page = "team"
}
}
Adding a user to the Owners team grants organization owner privileges
When a user joins the Owners team, OrgUser.IsOwner is set to true. Therefore, adding a user to the Owners team directly results in granting organization owner–equivalent privileges.
internal/database/org_team.go:566-576
ou := new(OrgUser)
if _, err = sess.Where("uid = ?", userID).
And("org_id = ?", orgID).Get(ou); err != nil {
return err
}
ou.NumTeams++
if t.IsOwnerTeam() {
ou.IsOwner = true
}
if _, err = sess.ID(ou.ID).AllCols().Update(ou); err != nil {
return err
}
Related issue: organization member actions are also state-changing via GET
For reference, organization member management endpoints are also exposed as GET routes that perform state changes without CSRF protection.
internal/cmd/web.go:382
m.Get("/members/action/:action", org.MembersAction)
MembersAction similarly does not branch on HTTP method and performs state-changing operations (public/private toggle, remove, leave) based on query parameters and the :action path parameter.
internal/route/org/members.go:31-71
func MembersAction(c *context.Context) {
uid := com.StrTo(c.Query("uid")).MustInt64()
if uid == 0 {
c.Redirect(c.Org.OrgLink + "/members")
return
}
org := c.Org.Organization
var err error
switch c.Params(":action") {
case "private":
err = database.ChangeOrgUserStatus(org.ID, uid, false)
case "public":
err = database.ChangeOrgUserStatus(org.ID, uid, true)
case "remove":
err = org.RemoveMember(uid)
case "leave":
err = org.RemoveMember(c.User.ID)
}
}
Steps to Reproduce
-
Prepare a target user account to be added (e.g.,
attacker). -
Confirm that the victim user is an owner of the target organization (e.g.,
org3) and is logged in. -
Cause the victim’s browser to perform a top-level navigation to the following URL:
http://localhost:10880/org/org3/teams/owners/action/add?uid=1&uname=attacker
- After the request completes, verify that the
attackeruser can access:
http://localhost:10880/org/org3/settings
confirming that organization owner privileges have been obtained.
Impact
Successful exploitation allows an attacker to obtain organization owner privileges, resulting in:
- Full control over organization repositories, settings, and members
- Unauthorized access to private repositories (confidentiality impact)
- Modification or deletion of repositories and settings (integrity impact)
- Repository deletion or disruption leading to service unavailability (availability impact)
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "gogs.io/gogs"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.14.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-52800"
],
"database_specific": {
"cwe_ids": [
"CWE-352"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-23T00:02:44Z",
"nvd_published_at": "2026-06-24T21:16:55Z",
"severity": "HIGH"
},
"details": "## Summary\n\nIn **Gogs 0.14.1**, organization team member management can be performed via **GET requests without CSRF protection**.\nIf a victim who is an **organization owner** is logged in and is tricked into visiting a crafted link, an attacker-controlled user can be added to the **Owners** team. As a result, the attacker gains **organization owner\u2013equivalent privileges**.\n\n---\n\n## Description\n\nWhen a victim is logged in as an organization owner, **team member management endpoints are exposed via routes reachable by GET requests**, allowing state-changing operations without a CSRF token.\n\n### Team action route allows GET\n\n`internal/cmd/web.go:390`\n\n```go\nm.Route(\"/teams/:team/action/:action\", \"GET,POST\", org.TeamsAction)\n```\n\n### CSRF validation is applied only to POST requests\n\nBecause the global CSRF check is limited to POST requests, state-changing operations reached via GET bypass CSRF protection entirely.\n\n`internal/context/auth.go:56-61`\n\n```go\nif !options.SignOutRequired \u0026\u0026 !options.DisableCSRF \u0026\u0026\n c.Req.Method == \"POST\" \u0026\u0026 !isAPIPath(c.Req.URL.Path) {\n csrf.Validate(c.Context, c.csrf)\n if c.Written() {\n return\n }\n}\n```\n\n### TeamsAction performs state changes regardless of HTTP method\n\n`TeamsAction` does not branch on the HTTP method. Instead, it performs state-changing operations (such as adding or removing members) based solely on query parameters (`uid`, `uname`) and the `:action` path parameter.\nSince the route explicitly allows GET, the `add` action can be executed via GET.\n\n`internal/route/org/teams.go:38-83`\n\n```go\nfunc TeamsAction(c *context.Context) {\n uid := com.StrTo(c.Query(\"uid\")).MustInt64()\n if uid == 0 {\n c.Redirect(c.Org.OrgLink + \"/teams\")\n return\n }\n\n page := c.Query(\"page\")\n var err error\n switch c.Params(\":action\") {\n case \"add\":\n if !c.Org.IsOwner {\n c.NotFound()\n return\n }\n uname := c.Query(\"uname\")\n var u *database.User\n u, err = database.Handle.Users().GetByUsername(c.Req.Context(), uname)\n // ...\n err = c.Org.Team.AddMember(u.ID)\n page = \"team\"\n }\n}\n```\n\n### Adding a user to the Owners team grants organization owner privileges\n\nWhen a user joins the **Owners** team, `OrgUser.IsOwner` is set to `true`. Therefore, adding a user to the Owners team directly results in granting organization owner\u2013equivalent privileges.\n\n`internal/database/org_team.go:566-576`\n\n```go\nou := new(OrgUser)\nif _, err = sess.Where(\"uid = ?\", userID).\n And(\"org_id = ?\", orgID).Get(ou); err != nil {\n return err\n}\nou.NumTeams++\nif t.IsOwnerTeam() {\n ou.IsOwner = true\n}\nif _, err = sess.ID(ou.ID).AllCols().Update(ou); err != nil {\n return err\n}\n```\n\n### Related issue: organization member actions are also state-changing via GET\n\nFor reference, organization member management endpoints are also exposed as GET routes that perform state changes without CSRF protection.\n\n`internal/cmd/web.go:382`\n\n```go\nm.Get(\"/members/action/:action\", org.MembersAction)\n```\n\n`MembersAction` similarly does not branch on HTTP method and performs state-changing operations (public/private toggle, remove, leave) based on query parameters and the `:action` path parameter.\n\n`internal/route/org/members.go:31-71`\n\n```go\nfunc MembersAction(c *context.Context) {\n uid := com.StrTo(c.Query(\"uid\")).MustInt64()\n if uid == 0 {\n c.Redirect(c.Org.OrgLink + \"/members\")\n return\n }\n\n org := c.Org.Organization\n var err error\n switch c.Params(\":action\") {\n case \"private\":\n err = database.ChangeOrgUserStatus(org.ID, uid, false)\n case \"public\":\n err = database.ChangeOrgUserStatus(org.ID, uid, true)\n case \"remove\":\n err = org.RemoveMember(uid)\n case \"leave\":\n err = org.RemoveMember(c.User.ID)\n }\n}\n```\n\n---\n\n## Steps to Reproduce\n\n1. Prepare a target user account to be added (e.g., `attacker`).\n\n2. Confirm that the victim user is an **owner** of the target organization (e.g., `org3`) and is logged in.\n\n3. Cause the victim\u2019s browser to perform a **top-level navigation** to the following URL:\n\n ```\n http://localhost:10880/org/org3/teams/owners/action/add?uid=1\u0026uname=attacker\n ```\n\u003cimg width=\"2019\" height=\"322\" alt=\"image\" src=\"https://github.com/user-attachments/assets/342a627a-04e8-47bd-818a-9c2b05a75446\" /\u003e\n\n\n4. After the request completes, verify that the `attacker` user can access:\n\n ```\n http://localhost:10880/org/org3/settings\n ```\n\n confirming that organization owner privileges have been obtained.\n\n\u003cimg width=\"2010\" height=\"285\" alt=\"image\" src=\"https://github.com/user-attachments/assets/03945bb1-e9c5-4e42-ad3a-9f6d63b7d86d\" /\u003e\n\n\n\u003cimg width=\"2016\" height=\"893\" alt=\"image\" src=\"https://github.com/user-attachments/assets/55d7db13-52cf-471b-a6d3-aa4186c8b547\" /\u003e\n\n\n\n\n---\n\n## Impact\n\nSuccessful exploitation allows an attacker to obtain **organization owner privileges**, resulting in:\n\n* Full control over organization repositories, settings, and members\n* Unauthorized access to private repositories (confidentiality impact)\n* Modification or deletion of repositories and settings (integrity impact)\n* Repository deletion or disruption leading to service unavailability (availability impact)",
"id": "GHSA-pwx3-qcgw-vh7h",
"modified": "2026-07-21T13:36:47Z",
"published": "2026-06-23T00:02:44Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/gogs/gogs/security/advisories/GHSA-pwx3-qcgw-vh7h"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-52800"
},
{
"type": "WEB",
"url": "https://github.com/gogs/gogs/pull/8321"
},
{
"type": "WEB",
"url": "https://github.com/gogs/gogs/commit/070df61ecd14c75b0aca93090f860b87ab17ac19"
},
{
"type": "PACKAGE",
"url": "https://github.com/gogs/gogs"
},
{
"type": "WEB",
"url": "https://github.com/gogs/gogs/releases/tag/v0.14.3"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "Gogs Vulnerable to CSRF Leading to Organization Owner Takeover"
}
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.
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
How durations are calculated
Following the VTEM framework (Black Hat USA 2015), time to patch starts when the vendor or a coordinator was informed, total time to patch at discovery, and disclosure to patch at public disclosure; vendor response time runs from the report to its acknowledgement. Time to exploit and time unpatched start at the earliest discovery or report, falling back to public disclosure. Unpatched time ends at the first recorded patch, or the calculation time when no patch is recorded. Exploitation to patch shows the interval between those two events; a published proof of concept is a public exploit, not exploitation.
Each result shows its source dates. A negative interval means the end event came first; when date-only inputs leave the order open, the result says so. Date-only inputs are approximate. The earliest evidence wins: KEV report dates and sightings are labelled as such. Records describing another vulnerability are listed but never calculated from, and a PoC alone does not establish exploitation. Missing patch evidence does not prove that no patch exists. Filters below change the event list only.
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
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.