Skip to main content

Gitea CVE-2026-58510

| EUVDEUVD-2026-58123 MEDIUM
Information Exposure (CWE-200)
2026-07-21 https://github.com/go-gitea/gitea GHSA-q423-49rw-g9mh
4.3
CVSS 3.1 · Vendor: https://github.com/go-gitea/gitea
View

Attack ChainAIDerived

Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain

  1. Access
    Authenticate to Gitea with prior watcher credentials
  2. Delivery
    Operator triggers PATCH /api/v1/repos/{owner}/{repo} with private:true via API automation
  3. Exploit
    updateRepository runs ClearRepoStars but skips ClearRepoWatches
  4. Execution
    Stale watch row persists in database
  5. Persist
    Query GET /api/v1/user/subscriptions
  6. Impact
    Receive full Repository JSON metadata for now-private repository

Vulnerability Assessment

Weakness
CWE
OWASP Top 10:2025
A01:2025 - Broken Access Control
ASVS 5.0
V14 - Data Protection Data exposure prevention
MITRE ATT&CK

2 techniques across 2 tactics: Collection, Discovery

The technique list with ATT&CK links and the kill-chain view is part of the Basic plan and up.

Exploitation Conditions

Exploitation requires a network-authenticated Gitea user (PR:L) who previously watched the target repository while it was public, creating the stale watch record that is never cleaned up. The critical limiting condition is that the repository visibility change must occur via the REST API (PATCH /api/v1/repos/{owner}/{repo} with body {"private": true}) rather than the web UI Settings → Danger Zone path; repositories made private through the web UI invoke MakeRepoPrivate, which correctly calls ClearRepoWatches and is not vulnerable. The Gitea instance must run v1.25.4 through v1.26.x. No elevated privileges beyond a standard authenticated user account are required - the attacker queries their own subscription list (GET /api/v1/user/subscriptions), which requires only their own credentials. Exploitation is bounded to repository metadata disclosure (description, branch names, topic tags, fork/issue/release counts, mirror metadata) and cannot expose repository content, commits, code, or issues. The stale watch row itself is the prerequisite artifact, not any active attack preparation.

Impact

The CVSS 3.1 base score of 4.3 Medium (AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N) accurately characterizes the bounded impact: network-accessible, low-complexity exploitation requires authenticated access (PR:L) and produces only metadata disclosure, not content access. No CISA KEV listing is present, confirming no confirmed active exploitation at time of analysis. The advisory provides complete, detailed reproduction steps described by the reporter as derivable purely from static analysis, which means functional exploit construction requires minimal effort - however, this is distinct from confirmed active exploitation. Real-world risk is disproportionately elevated for organizations that use IaC automation (terraform-gitea), official SDKs (go-sdk, py-gitea), or API-driven repository lifecycle tooling, since these tools canonically exercise the unpatched API path rather than the web UI. Multi-tenant Gitea-as-a-service operators face the broadest exposure as their tenants may independently trigger the vulnerable path. The send-time mail filter added in GHSA-8fwc-qjw5-rvgp partially mitigates the notification-content disclosure vector, but the metadata leak via the subscriptions endpoint and stale NumWatches inflation remain unmitigated until upgrade. EPSS data was not provided; given the narrow exploitation prerequisites (valid credentials plus prior watch record), widespread opportunistic exploitation is unlikely.

Exploit ScenarioDerived

A former collaborator who watched a public Gitea repository before it was made private via an API-driven automation script (e.g., a Terraform apply using terraform-gitea) retains their watch record due to the missing ClearRepoWatches call. Using only their own valid Gitea credentials, they query GET /api/v1/user/subscriptions and receive the full Repository JSON - including description, default branch, topic tags, programming language, fork/issue/release counts, and mirror metadata - for a repository they no longer have explicit access to. If the organization later publishes a release to the now-private repository, stale watch rows may also re-trigger notification paths that lack the send-time access check, potentially leaking release content.

This Month Plan within 30 days. Why: MEDIUM severity (CVSS 4.0-6.9).
Patch available 0.2% exploitation probability (EPSS)

Get an e-mail when this CVE is exploited, patched or re-scored. Free account.

Alert me

AnalysisAIv1 – Jul 22, 2026

Gitea's REST API path for making repositories private omits the ClearRepoWatches cleanup present in the web UI path, exposing private repository metadata to ex-watchers from v1.25.4 through v1.26.x. When the visibility transition occurs via PATCH /api/v1/repos/{owner}/{repo} - the canonical path used by terraform-gitea, official SDKs, and CI/CD automation - the updateRepository function clears stars but leaves watch records intact, allowing former watchers to retrieve the full Repository JSON payload (description, branch, language, topics, counts, mirror metadata) via GET /api/v1/user/subscriptions despite having no access to the repository. This is a confirmed incomplete fix relative to GHSA-8fwc-qjw5-rvgp; no public exploit identified at time of analysis, though the advisory provides complete reproduction steps directly derivable from static code analysis.

RemediationAI

Upgrade to Gitea v1.27.0, available at https://github.com/go-gitea/gitea/releases/tag/v1.27.0, which adds the missing repo_model.ClearRepoWatches call to updateRepository in services/repository/repository.go, mirroring the fix already present in MakeRepoPrivate. Vendor advisory: https://github.com/go-gitea/gitea/security/advisories/GHSA-q423-49rw-g9mh. If immediate upgrade is not feasible, operators should restrict API write access to PATCH /api/v1/repos/ endpoints to administrative users only via an authenticating reverse proxy - this blocks all API-driven repository management, not just visibility changes, which may break automation. As a parallel remediation for already-affected instances, audit stale watch records by querying GET /repos/{owner}/{repo}/subscribers for all private repositories transitioned via API since v1.25.4 and removing stale rows; this can be performed via the Gitea API or directly in the database (DELETE FROM watch WHERE repo_id = ? AND mode = 1 for each affected repository ID). The database cleanup eliminates the metadata leak immediately but must be applied per-repository and does not prevent recurrence before the code fix is deployed.

Affected VersionsENISA

Gitea Open Source Git Server 0 ≤1.26.4

Affected Packages

go code.gitea.io/gitea fix: 1.27.0
go gitea.dev fix: 1.27.0
euvd gitea_open_source_git_server no fix

Source: OSV (Open Source Vulnerabilities)

Affects your stack

Affects 3 packages in go, euvd

Checking this CVE against your own stacks and servers is part of the Basic plan and up.

Technical ContextAI · show details

Gitea is a self-hosted Git service written in Go (CPE: pkg:go/code.gitea.io/gitea). The root cause is CWE-281 (Improper Preservation of Permissions): the access-revocation logic introduced by GHSA-8fwc-qjw5-rvgp (PR #36319, commit 8a98ac22) was wired into MakeRepoPrivate in services/repository/repository.go (line 157) but was never mirrored into the sibling function updateRepository (lines 240-302) in the same file. updateRepository clears stars on line 273 but never calls repo_model.ClearRepoWatches. The REST API call chain runs through routers/api/v1/repo/repo.go::Edit (line 573) → updateBasicProperties (line 678-697) → repo_service.UpdateRepository → updateRepository, meaning every API-driven visibility change bypasses the cleanup. A subsequent refactor (PR #36959, commit 943ff752, 2026-03-24) merged MakeRepoPublic and MakeRepoPrivate but did not propagate ClearRepoWatches to updateRepository. The test asymmetry in repository_test.go confirms the gap: TestMakeRepoPrivateClearsWatches asserts watches are cleared, while TestUpdateRepositoryVisibilityChanged asserts only IsPrivate without verifying GetRepoWatchersIDs returns empty. Secondary CWEs CWE-359 (Exposure of Private Personal Information) and CWE-200 (Exposure of Sensitive Information) apply to the metadata leakage outcome.

Know when Gitea gets its next vulnerability

  • Watch products and get an email when something new is published
  • Check this CVE against your own stack and servers
  • Exploit and PoC references, threat intelligence from 22 sources

No credit card · 7-day full trial

Severity by source

Vendor (https://github.com/go-gitea/gitea) PRIMARY 4.3 MEDIUM
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N
vuln.today AI 4.3 MEDIUM

Authenticated network access required (PR:L); impact limited to repository metadata enumeration via own subscriptions endpoint, not code or content (C:L); no integrity or availability impact.

3.1 AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N
4.0 AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N
SUSE MEDIUM

Primary rating from Vendor (https://github.com/go-gitea/gitea).

CVSS VectorVendor: https://github.com/go-gitea/gitea

Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
None
Availability
None

DescriptionCVE.org

Summary

GHSA-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.

As 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:

  • 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.
  • 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.
  • Inflate the visible NumWatches counter on the repository.

Severity

Medium - CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N

Same 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.

CWE-281 (Improper Preservation of Permissions), CWE-359 (Exposure of Private Personal Information), CWE-200 (Exposure of Sensitive Information).

Affected Versions

Every 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.

Affected Component

  • 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.
  • 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.

Steps to Reproduce

The bug is observable purely from static analysis; the live PoC is straightforward.

  1. Start a Gitea instance at any release from v1.25.4 onwards (verified static at HEAD ef801bb6).
  2. Create users A (org admin) and B (member). Create a public repository A/proj. As B, watch the repo:
   curl -u B:<token> -X PUT "https://gitea.example.com/api/v1/repos/A/proj/subscription"
  1. As A, flip the repo to private via the REST API (not via the web UI):
   curl -u A:<token> -X PATCH "https://gitea.example.com/api/v1/repos/A/proj" \
        -H 'Content-Type: application/json' \
        -d '{"private": true}'
  1. As B, list B's watched repos with private=true:
   curl -u B:<token> "https://gitea.example.com/api/v1/user/subscriptions"

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.

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

The asymmetry of outcomes between steps 4 and 5 - for the same state transition - is the gap.

Direct Evidence (no PoC needed)

$ gh api repos/go-gitea/gitea/contents/services/repository/repository.go \
    --jq .content | base64 -d | grep -n "ClearRepoWatches\|ClearRepoStars"

154:                  if err = repo_model.ClearRepoStars(ctx, repo.ID); err != nil {
157:                  if err = repo_model.ClearRepoWatches(ctx, repo.ID); err != nil {
# MakeRepoPrivate
273:                  if err = repo_model.ClearRepoStars(ctx, repo.ID); err != nil {
# updateRepository - ClearRepoWatches missing here

The fix-author's own test file confirms the asymmetry:

$ gh api repos/go-gitea/gitea/contents/services/repository/repository_test.go --jq .content | base64 -d | grep -n "Test.*VisibilityChanged\|Test.*ClearsWatches"

44:func TestUpdateRepositoryVisibilityChanged(t *testing.T) {
# only checks act.IsPrivate
73:func TestMakeRepoPrivateClearsWatches(t *testing.T) {
# checks watches are cleared

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.

Impact

  • An organization that uses terraform-gitea or any other REST-API-driven automation to flip repositories private (the canonical IaC pattern) hits updateRepository, not MakeRepoPrivate.
  • 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.
  • Multi-tenant Gitea-as-a-service operators with API-driven repository-lifecycle endpoints are exposed.

Stale 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.

Suggested Fix

Drop-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:

go
// services/repository/repository.go
func updateRepository(ctx context.Context, repo *repo_model.Repository, visibilityChanged bool) (err error) {
    ...
    if visibilityChanged {
        ...
        // If repo has become private, we need to set its actions to private.
        if repo.IsPrivate {
            _, err = e.Where("repo_id = ?", repo.ID).Cols("is_private").Update(&activities_model.Action{
                IsPrivate: true,
            })
            if err != nil {
                return err
            }

            if err = repo_model.ClearRepoStars(ctx, repo.ID); err != nil {
                return err
            }

            // Match MakeRepoPrivate's behavior - see PR #36319 / GHSA-8fwc-qjw5-rvgp.
            // Stale watch rows on a now-private repo leak repository metadata to ex-watchers
            // via GET /user/subscriptions?private=true and through any future notification
            // path that does not have a send-time access check.
            if err = repo_model.ClearRepoWatches(ctx, repo.ID); err != nil {
                return err
            }
        }
        ...
    }
    ...
}

Regression test (mirror of TestMakeRepoPrivateClearsWatches) to add to services/repository/repository_test.go:

go
func TestUpdateRepositoryClearsWatchesOnVisibilityChange(t *testing.T) {
    assert.NoError(t, unittest.PrepareTestDatabase())

    repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
    assert.False(t, repo.IsPrivate)

    watchers, err := repo_model.GetRepoWatchersIDs(t.Context(), repo.ID)
    require.NoError(t, err)
    require.NotEmpty(t, watchers)

    repo.IsPrivate = true
    assert.NoError(t, updateRepository(t.Context(), repo, true))

    watchers, err = repo_model.GetRepoWatchersIDs(t.Context(), repo.ID)
    assert.NoError(t, err)
    assert.Empty(t, watchers)

    updatedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID})
    assert.Zero(t, updatedRepo.NumWatches)
}

Optional 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.

Discovery Methodology

This 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.

For Gitea:

  1. Enumerate recent advisories via gh api graphql ... securityVulnerabilities(ecosystem: GO, package: code.gitea.io/gitea).
  2. 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.
  3. gh api repos/.../commits/8a98ac221 --jq '.files[] | .filename' listed the fix files; ClearRepoWatches was added to one function.
  4. 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).
  5. Walked the call chain from routers/api/v1/repo/repo.go::Edit to confirm the API path uses updateRepository, not MakeRepoPrivate.

Pre-emptive rebuttals

  • "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.
  • "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.
  • "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.

References

  • GHSA-8fwc-qjw5-rvgp - the original advisory: https://github.com/go-gitea/gitea/security/advisories/GHSA-8fwc-qjw5-rvgp
  • PR #36319 "clean watches when make a repository private and check permission when send release emails" (commit 8a98ac221)
  • PR #36959 "Require additional user confirmation for making repo private" (commit 943ff7523) - the later refactor that did not propagate the cleanup
  • Patched function: services/repository/repository.go::MakeRepoPrivate (line 125, calls ClearRepoWatches line 157)
  • Unpatched sister: services/repository/repository.go::updateRepository (line 240, calls only ClearRepoStars line 273)
  • API entry: routers/api/v1/repo/repo.go::Edit → updateBasicProperties → repo_service.UpdateRepository → updateRepository
  • Test asymmetry: services/repository/repository_test.go::TestMakeRepoPrivateClearsWatches (covered) vs TestUpdateRepositoryVisibilityChanged (does not assert watches cleared)

Threat Intelligence Overview

EPSS Score
0.2%
Top 91% exploitable
Prevalence
Medium
popular self-hosted Git platform used widely by dev teams and enterprises
Threat Intel
1
1 source
Exploit
POC
Public exploit code exists
Data Quality
6 sources
Confidence: High

How this decision was reached

The engine reads each signal below and applies a fixed list of rules from top to bottom. The first rule that matches sets the recommended action – like a statement, every line is shown.

SignalValue
Severity MEDIUM
CISA KEV No
Public POC Yes
EPSS 0.2%
Patch available Yes
Active threat No
Deciding rule
MEDIUM severity (CVSS 4.0-6.9)
What every action level means
Emergency Immediate action required. Criteria: CRITICAL + in CISA KEV (actively exploited) without patch, OR CRITICAL + EPSS > 30% without patch, OR CRITICAL + active threat signals without patch.
Act Now Prioritize within 24-48h. Criteria: in KEV with patch available, OR CRITICAL + public POC, OR HIGH + EPSS > 10%, OR CRITICAL + active threat, OR any CRITICAL severity.
This Week Schedule remediation this week. Criteria: HIGH severity (CVSS 7.0-8.9). Patch within normal change window.
This Month Plan within 30 days. Criteria: MEDIUM severity (CVSS 4.0-6.9), lower exploitation probability. Include in regular patch cycle.
Monitor Track and reassess. Criteria: LOW severity (CVSS 0.1-3.9), minimal exploitation risk. No immediate action needed.

SSVC AssessmentENISA

SSVC Decision· derived
Scheduled
Exploitation· derived
Poc
System Exposure· derived
Open
Automatable· derived
No
Human Impact· derived
Medium

Priority Score

42
Low Medium High Critical
KEV: 0
EPSS: 0
CVSS: +22
PoC: +20
Weaponized: 0
Threat intel: 0
Gov advisory: 0

Attack Technique

Information disclosure occurs when an application unintentionally exposes sensitive data that aids attackers in reconnaissance or directly compromises security.

More in Gitea

View all
CVE-2026-60004 CRITICAL POC
9.8

Remote code execution in Gitea (self-hosted Git service) via a code-injection flaw (CWE-94) allows attackers to run arbi

CVE-2026-27771 HIGH POC
8.2 Jul 03

Broken access control in Gitea's Composer package registry (versions up to and including 1.26.1) lets remote attackers r

CVE-2022-30781 HIGH POC
7.5 May 16

Gitea before 1.16.7 does not escape git fetch remote. Rated high severity (CVSS 7.5), this vulnerability is remotely exp

CVE-2020-14144 HIGH POC
7.2 Oct 16

The git hook feature in Gitea 1.1.0 through 1.12.5 might allow for authenticated remote code execution in customer envir

CVE-2024-6886 CRITICAL POC
10.0 Aug 06

Improper Neutralization of Input During Web Page Generation (XSS or 'Cross-site Scripting') vulnerability in Gitea Gitea

CVE-2026-72867 CRITICAL POC
9.9 Aug 10

Authenticated command injection in Dokploy (self-hostable PaaS) versions 0.29.3 through 0.29.12 lets a low-privileged us

CVE-2026-20896 CRITICAL POC
9.8 Jul 03

Reverse-proxy authentication bypass in the official Gitea Docker image (versions up to and including 1.26.2) allows any

CVE-2026-59774 CRITICAL POC
9.8

Path traversal in Gitea's Org-mode (`.org`) markup renderer allows an attacker who can get Org-mode content rendered by

CVE-2026-58053 CRITICAL POC
9.4 Jun 28

Container escape in Gitea act_runner (Docker backend, through act 0.262.0) lets an authenticated user with workflow-exec

CVE-2019-11229 HIGH POC
8.8 Apr 15

models/repo_mirror.go in Gitea before 1.7.6 and 1.8.x before 1.8-RC3 mishandles mirror repo URL settings, leading to rem

CVE-2026-57894 HIGH POC
8.5 Jul 21

Server-side request forgery and internal repository exfiltration in Gitea before 1.27.0 lets a low-privileged authentica

CVE-2026-24791 HIGH POC
8.1 Jun 17

Authorization bypass in Gitea versions 1.22.3 through 1.26.1 allows holders of `public-only` access tokens or OAuth gran

Vendor StatusVendor

SUSE

Severity: Moderate
Product Status
SUSE Linux Enterprise Server 16.1 Affected
SUSE Linux Enterprise Server for SAP applications 16.1 Affected

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy