CVE-2026-58510: Gitea GHSA-8fwc-qjw5-rvgp ClearRepoWatches Fix Not Applied Vulnerability
MODERATECVE-2026-58510 is a MODERATE (CVSS 4.3) ghsa-8fwc-qjw5-rvgp clearrepowatches fix not applied vulnerability (CWE-200: Exposure of Sensitive Information to an Unauthorized Actor) affecting Gitea libraries in the Go ecosystem. The issue involves ghsa-8fwc-qjw5-rvgp clearrepowatches fix not applied to api editrepo path — sister code path retains stale watches on public->private. Remediation is available by upgrading affected packages to version 1.27.0.
CVE-2026-58510 at a Glance
Security Impact
CVSS: 4.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:N/A:N).
How to Fix CVE-2026-58510
To remediate this vulnerability, upgrade all affected Gitea dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
Upgrade code.gitea.io/gitea to 1.27.0
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
inspect dependencies for code.gitea.io/gitea
How to Check if You Are Affected
To determine whether your application is affected by CVE-2026-58510, inspect your project dependencies for Gitea:
inspect dependencies for code.gitea.io/gitea
Analyze the terminal output to locate any version of Gitea in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.27.0), 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-200: Exposure of Sensitive Information to an Unauthorized Actor, where the product exposes sensitive information to an actor that is not explicitly authorized to have access to that information. In Gitea, 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
CVE-2026-58510 Affected Gitea Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
code.gitea.io/gitea
|
Go |
< 1.27.0
|
1.27.0
|
References
Frequently Asked Questions
What is CVE-2026-58510?
Which Gitea versions are affected?
What version fixes CVE-2026-58510?
Is code.gitea.io/gitea affected?
What is the CVSS score of CVE-2026-58510?
What CWE is associated with CVE-2026-58510?
How can I check Go dependencies for CVE-2026-58510?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-q423-49rw-g9mh",
"modified" : "2026-07-21T21:51:41Z",
"published" : "2026-07-21T21:51:41Z",
"aliases" : [ "CVE-2026-58510" ],
"summary" : "Gitea: GHSA-8fwc-qjw5-rvgp ClearRepoWatches fix not applied to API EditRepo path — sister code path retains stale watches on public->private",
"details" : "## Summary\n\nGHSA-8fwc-qjw5-rvgp (\"Gitea may send release notification emails for private repositories to users whose access has been revoked\", fix in PR #36319 / commit 8a98ac22) added `repo_model.ClearRepoWatches` as a defense for the state transition public→private. The cleanup was wired into `services/repository/repository.go::MakeRepoPrivate` only. The sister helper `services/repository/repository.go::updateRepository` — which is the function used by the API path `PATCH /api/v1/repos/{owner}/{repo}` — was not patched and still calls `ClearRepoStars` only.\n\nAs a result, when a public repository is flipped to private via the REST API (rather than via the web Settings → Danger Zone UI), the watch records persist. Affected users can:\n\n- See the now-private repository in `GET /api/v1/user/subscriptions?private=true` along with its full `Repository` JSON (description, default branch, language, fork status, counts, mirror metadata, license list, etc.) — even though they have no access to the repository.\n- Have their stale watch records re-leak content through any future notification path that does not include the send-time `CheckRepoUnitUser` check that was added to `services/mailer/mail_release.go`.\n- Inflate the visible `NumWatches` counter on the repository.\n\n## Severity\n\nMedium — `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N`\n\nSame impact class as the original GHSA-8fwc-qjw5-rvgp (which was classified Medium). The send-time mail filter added in the same PR mitigates the release-content disclosure vector. The residual leak is repo metadata via the subscriptions endpoint and stale watcher counts.\n\nCWE-281 (Improper Preservation of Permissions), CWE-359 (Exposure of Private Personal Information), CWE-200 (Exposure of Sensitive Information).\n\n## Affected Versions\n\nEvery release starting from v1.25.4 (the release shipping the original GHSA-8fwc-qjw5-rvgp fix) through HEAD (master @ `ef801bb6`, 2026-05-16). The follow-up refactor in commit `943ff752` (PR #36959, 2026-03-24) which merged `MakeRepoPublic`+`MakeRepoPrivate` did not propagate the `ClearRepoWatches` call to `updateRepository`.\n\n## Affected Component\n\n- `services/repository/repository.go:240-302` — `func updateRepository(ctx, repo, visibilityChanged bool)`, the sister helper called via the API path. Clears stars on line 273 but never calls `ClearRepoWatches`.\n- `routers/api/v1/repo/repo.go::Edit` (line 573) → `updateBasicProperties` (line 678-697) → `repo_service.UpdateRepository(ctx, repo, visibilityChanged)` (line 726) — the API path that exercises the sister helper.\n\n## Steps to Reproduce\n\nThe bug is observable purely from static analysis; the live PoC is straightforward.\n\n1. Start a Gitea instance at any release from v1.25.4 onwards (verified static at HEAD `ef801bb6`).\n\n2. Create users `A` (org admin) and `B` (member). Create a public repository `A/proj`. As `B`, watch the repo:\n ```\n curl -u B:<token> -X PUT \"https://gitea.example.com/api/v1/repos/A/proj/subscription\"\n ```\n\n3. As `A`, flip the repo to private via the REST API (not via the web UI):\n ```\n curl -u A:<token> -X PATCH \"https://gitea.example.com/api/v1/repos/A/proj\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"private\": true}'\n ```\n\n4. As `B`, list `B`'s watched repos with `private=true`:\n ```\n curl -u B:<token> \"https://gitea.example.com/api/v1/user/subscriptions\"\n ```\n\n The now-private repo `A/proj` is returned in `B`'s subscription list, with the full `Repository` payload — including `description`, `default_branch`, `language`, `topics`, `license`, fork/branch/issue/release counts, etc.\n\n5. Compare with the web-UI path (which IS patched). As `A`, flip a different public repo `A/proj2` to private via Settings → Danger Zone → \"Make this repository private\" (which calls `repo_service.MakeRepoPrivate`). Verify `B`'s subscription list no longer contains `A/proj2`.\n\nThe asymmetry of outcomes between steps 4 and 5 — for the same state transition — is the gap.\n\n## Direct Evidence (no PoC needed)\n\n```\n$ gh api repos/go-gitea/gitea/contents/services/repository/repository.go \\\n --jq .content | base64 -d | grep -n \"ClearRepoWatches\\|ClearRepoStars\" \n\n154: if err = repo_model.ClearRepoStars(ctx, repo.ID); err != nil {\n157: if err = repo_model.ClearRepoWatches(ctx, repo.ID); err != nil { # MakeRepoPrivate\n273: if err = repo_model.ClearRepoStars(ctx, repo.ID); err != nil { # updateRepository — ClearRepoWatches missing here\n```\n\nThe fix-author's own test file confirms the asymmetry:\n\n```\n$ gh api repos/go-gitea/gitea/contents/services/repository/repository_test.go --jq .content | base64 -d | grep -n \"Test.*VisibilityChanged\\|Test.*ClearsWatches\"\n\n44:func TestUpdateRepositoryVisibilityChanged(t *testing.T) { # only checks act.IsPrivate\n73:func TestMakeRepoPrivateClearsWatches(t *testing.T) { # checks watches are cleared\n```\n\n`TestUpdateRepositoryVisibilityChanged` explicitly calls `updateRepository(ctx, repo, true)` (line 53) and asserts `act.IsPrivate` (line 61) — but never verifies `GetRepoWatchersIDs` returns empty, while the parallel `TestMakeRepoPrivateClearsWatches` does. The test asymmetry mirrors the fix asymmetry.\n\n## Impact\n\n- An organization that uses [terraform-gitea](https://github.com/go-gitea/terraform-provider-gitea) or any other REST-API-driven automation to flip repositories private (the canonical IaC pattern) hits `updateRepository`, not `MakeRepoPrivate`.\n- An organization using the official Gitea SDK (`go-sdk`, `py-gitea`, etc.) or `curl` scripts to make repos private after an internal policy change hits the same path.\n- Multi-tenant Gitea-as-a-service operators with API-driven repository-lifecycle endpoints are exposed.\n\nStale watch rows leak through `GET /user/subscriptions` (with the watcher's own credentials), `GET /repos/{owner}/{repo}/subscribers` (stale `NumWatches` total), and become a re-leak surface for any future notification path that forgets the send-time access check.\n\n## Suggested Fix\n\nDrop-in mirror of the call already present in `MakeRepoPrivate`. In `services/repository/repository.go::updateRepository`, inside the existing `if repo.IsPrivate { ... }` branch (around line 265-276), add the `ClearRepoWatches` call directly after `ClearRepoStars`:\n\n```go\n// services/repository/repository.go\nfunc updateRepository(ctx context.Context, repo *repo_model.Repository, visibilityChanged bool) (err error) {\n ...\n if visibilityChanged {\n ...\n // If repo has become private, we need to set its actions to private.\n if repo.IsPrivate {\n _, err = e.Where(\"repo_id = ?\", repo.ID).Cols(\"is_private\").Update(&activities_model.Action{\n IsPrivate: true,\n })\n if err != nil {\n return err\n }\n\n if err = repo_model.ClearRepoStars(ctx, repo.ID); err != nil {\n return err\n }\n\n // Match MakeRepoPrivate's behavior — see PR #36319 / GHSA-8fwc-qjw5-rvgp.\n // Stale watch rows on a now-private repo leak repository metadata to ex-watchers\n // via GET /user/subscriptions?private=true and through any future notification\n // path that does not have a send-time access check.\n if err = repo_model.ClearRepoWatches(ctx, repo.ID); err != nil {\n return err\n }\n }\n ...\n }\n ...\n}\n```\n\nRegression test (mirror of `TestMakeRepoPrivateClearsWatches`) to add to `services/repository/repository_test.go`:\n\n```go\nfunc TestUpdateRepositoryClearsWatchesOnVisibilityChange(t *testing.T) {\n assert.NoError(t, unittest.PrepareTestDatabase())\n\n repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})\n assert.False(t, repo.IsPrivate)\n\n watchers, err := repo_model.GetRepoWatchersIDs(t.Context(), repo.ID)\n require.NoError(t, err)\n require.NotEmpty(t, watchers)\n\n repo.IsPrivate = true\n assert.NoError(t, updateRepository(t.Context(), repo, true))\n\n watchers, err = repo_model.GetRepoWatchersIDs(t.Context(), repo.ID)\n assert.NoError(t, err)\n assert.Empty(t, watchers)\n\n updatedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID})\n assert.Zero(t, updatedRepo.NumWatches)\n}\n```\n\nOptional belt-and-suspenders: an integration test against `PATCH /api/v1/repos/{owner}/{repo}` with body `{\"private\": true}` that exercises the entire API path through `routers/api/v1/repo/repo.go::Edit`.\n\n## Discovery Methodology\n\nThis finding follows the \"sister-fix-incomplete\" lens that has been productive across several recent reports: pick a recent advisory where the fix lands as a single PR touching one function, then grep the codebase for parallel call sites that should have received the same defense.\n\nFor Gitea:\n\n1. Enumerate recent advisories via `gh api graphql ... securityVulnerabilities(ecosystem: GO, package: code.gitea.io/gitea)`.\n2. GHSA-8fwc-qjw5-rvgp stood out because the description names a state transition (public→private) — a class of bug where the defense is typically wired into a single helper.\n3. `gh api repos/.../commits/8a98ac221 --jq '.files[] | .filename'` listed the fix files; `ClearRepoWatches` was added to one function.\n4. `grep -rn \"ClearRepoWatches\\|MakeRepoPrivate\"` revealed two functions in `services/repository/repository.go` that handle the state transition: `MakeRepoPrivate` (patched) and the lowercase sister `updateRepository` (unpatched).\n5. Walked the call chain from `routers/api/v1/repo/repo.go::Edit` to confirm the API path uses `updateRepository`, not `MakeRepoPrivate`.\n\n## Pre-emptive rebuttals\n\n- \"The send-time filter in `MailNewRelease` already blocks release-content disclosure\" — Correct, and acknowledged. The residual leak this report concerns is metadata via the `GET /user/subscriptions` endpoint and stale `NumWatches`. The send-time filter is a necessary but not sufficient defense; the `ClearRepoWatches` call is the persistence-side belt-and-suspenders that the original PR author explicitly added.\n- \"The web UI is the supported path; the API path is not in scope\" — The API is documented public surface (swagger spec in `templates/swagger/v1_json.tmpl`), Gitea ships official SDKs (`go-sdk`, `py-gitea`) that use exactly this path, and there is an official Terraform provider that exercises it. The existing fix is in a sister helper used by both paths' upstream helper — the fix author plainly intended to defend both.\n- \"AccessMode is `None` in the subscription response, so the client should infer no-access\" — The response still returns the full `Repository` payload including description, default branch, language, fork status, counts, mirror metadata, OriginalURL, license, etc. (see `services/convert/repository.go::innerToRepo` line 189-259). `AccessMode: 0` does not gate the metadata fields, only the `Permissions{Admin,Push,Pull}` triple.\n\n## References\n\n- GHSA-8fwc-qjw5-rvgp — the original advisory: https://github.com/go-gitea/gitea/security/advisories/GHSA-8fwc-qjw5-rvgp\n- PR #36319 \"clean watches when make a repository private and check permission when send release emails\" (commit `8a98ac221`)\n- PR #36959 \"Require additional user confirmation for making repo private\" (commit `943ff7523`) — the later refactor that did not propagate the cleanup\n- Patched function: `services/repository/repository.go::MakeRepoPrivate` (line 125, calls `ClearRepoWatches` line 157)\n- Unpatched sister: `services/repository/repository.go::updateRepository` (line 240, calls only `ClearRepoStars` line 273)\n- API entry: `routers/api/v1/repo/repo.go::Edit` → `updateBasicProperties` → `repo_service.UpdateRepository` → `updateRepository`\n- Test asymmetry: `services/repository/repository_test.go::TestMakeRepoPrivateClearsWatches` (covered) vs `TestUpdateRepositoryVisibilityChanged` (does not assert watches cleared)",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "Go",
"name" : "code.gitea.io/gitea"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "1.27.0"
} ]
} ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/go-gitea/gitea/security/advisories/GHSA-q423-49rw-g9mh"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/go-gitea/gitea"
}, {
"type" : "WEB",
"url" : "https://github.com/go-gitea/gitea/releases/tag/v1.27.0"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-200", "CWE-281", "CWE-359" ],
"severity" : "MODERATE",
"github_reviewed" : true,
"github_reviewed_at" : "2026-07-21T21:51:41Z",
"nvd_published_at" : null
}
}