Skip to content

Gitea push-to-create ignores ForcePrivate server policy, allowing non-admin users to publish repositories as public

Moderate
bircni published GHSA-fx95-gwfc-grgc Oct 6, 2026

Package

gomod gitea.dev (Go)

Affected versions

>= 1.27.0, <= 28.0.0

Patched versions

28.1.0

Description

Summary

The hookPostReceiveUpdateRepoByOptions function in Gitea's post-receive hook handler processes repo.private push options on empty repositories without checking the setting.Repository.ForcePrivate server policy. On instances where ForcePrivate is enabled, any authenticated user with repository admin permission (including regular repo creators) can make a repository public via git push -o repo.private=false, bypassing the site-level policy that every other visibility-change path enforces.

Root Cause

The ForcePrivate server policy is a site-level configuration (FORCE_PRIVATE = true in app.ini) intended to prevent non-site-admin users from making repositories public. Every visibility-change path in Gitea enforces this policy except the push option handler.

Affected function: hookPostReceiveUpdateRepoByOptions
File: routers/private/hook_post_receive.go (lines 143-188 in v1.27.3)

The function checks ctx.Repo.Permission.IsAdmin() (repo-level) at line 159
and repo.IsEmpty at line 166, but never checks setting.Repository.ForcePrivate.

Comparison with other paths that DO enforce the policy:
- API Edit: routers/api/v1/repo/repo.go line 696 checks ForcePrivate && !ctx.Doer.IsAdmin
- Web UI Settings: routers/web/repo/setting/setting.go line 1077 checks ForcePrivate && !ctx.Doer.IsAdmin
- Public Access: routers/web/repo/setting/public_access.go line 30 checks ForcePrivate
- Push-to-create service: services/repository/repository.go line 101 applies ForcePrivate at creation

Impact

When FORCE_PRIVATE = true is configured (a non-default, enterprise-oriented security setting), any authenticated user who can create repositories can bypass the policy on the push-to-create path. The repository is correctly created as private (the creation path enforces ForcePrivate), but the subsequent push option flips visibility to public. This makes the repository and its contents accessible to unauthenticated users, violating the organizational policy that all repositories must remain private.

The attacker must be an authenticated user with repository admin permission. Since all repository creators are automatically admins of their own repos, this is equivalent to any registered user. The attacker controls what content they push, so the confidentiality impact is limited to their own data becoming publicly visible in violation of policy. The integrity impact is the unauthorized modification of a security-relevant repository attribute.

ForcePrivate defaults to false, so only instances that have explicitly enabled this security policy are affected.

Attack Chain

  1. Exposure: An authenticated user pushes to an empty Gitea repository over HTTP or SSH. The post-receive hook fires for all pushes, including push-to-create.
  2. Control: The attacker includes -o repo.private=false as a push option. This value is read as a boolean at hook_post_receive.go:144 and directly controls the isPrivate variable.
  3. Path: The push option is forwarded via the internal API to hookPostReceiveUpdateRepoByOptions. The function loads the repo, checks repo-level admin permission (line 159), and checks repo.IsEmpty (line 166). For a first push to an empty repo, both checks pass.
  4. Guard: The missing check is setting.Repository.ForcePrivate. Unlike the API edit endpoint (repo.go:696) and the web UI settings endpoint (setting.go:1077), the push option handler does not check this server-level policy before allowing a private-to-public transition.
  5. Primitive: repo.IsPrivate = isPrivate.Value() at line 173, persisted via UpdateRepositoryColsNoAutoTime at line 175. The database record changes the repository's visibility.
  6. Result: The repository becomes publicly accessible. Anonymous users receive AccessModeRead permission because GetIndividualUserRepoPermission at repo_permission.go:466 grants read access when repo.IsPrivate is false and the user is nil (unauthenticated).

Bypass Evidence

This finding is an incomplete fix of CVE-2026-58437 (GHSA-8p9h-49rc-qgxj). The CVE fix added a !repo.IsEmpty guard at line 166 to restrict push options to empty repositories only, preventing visibility manipulation on populated repos. However, the fix did not add ForcePrivate enforcement on the remaining allowed path (empty repos during push-to-create).

The push option handler at hook_post_receive.go contains zero references to setting.Repository.ForcePrivate, ForcePrivate, or ctx.Doer.IsAdmin. This was confirmed by searching the entire file. Every other code path that changes repository visibility (API edit, web UI settings, public access settings) enforces the ForcePrivate policy by checking setting.Repository.ForcePrivate && !ctx.Doer.IsAdmin before allowing a private-to-public transition.

The strongest disproof attempted was whether applyPublicAccessPermission in the permission model provides a runtime backstop when ForcePrivate is enabled. Investigation showed it only controls per-unit anonymousAccessMode, not the primary perm.AccessMode which is set to AccessModeRead at repo_permission.go:466 when IsPrivate=false. No middleware-level ForcePrivate enforcement exists.

Affected Versions

  • Ecosystem: Go
  • Package: code.gitea.io/gitea
  • Confirmed affected range: >= 1.27.0, <= 1.27.3
  • Latest release checked: v1.27.3 (published 2026-08-29, Go module proxy)
  • Fix status: not fixed

The lower bound is v1.27.0 because that release introduced the CVE-2026-58437 fix (!repo.IsEmpty guard) which made the ForcePrivate gap independently exploitable. Before v1.27.0, the broader CVE-2026-58437 vulnerability (push options on all repos) subsumed this specific policy bypass. The ForcePrivate gap was never addressed in any release through v1.27.3.

Suggested Fix

Add a ForcePrivate check in hookPostReceiveUpdateRepoByOptions before allowing a private-to-public transition. The check should mirror the pattern used in the API edit endpoint: when ForcePrivate is enabled and the push option would make a private repo public, deny the change unless the pusher is a site admin (ctx.Doer.IsAdmin). This ensures the push option handler enforces the same server-level policy as all other visibility-change paths.

As an interim mitigation, administrators can disable push-to-create functionality (ENABLE_PUSH_CREATE_USER = false and ENABLE_PUSH_CREATE_ORG = false in app.ini) to eliminate the empty-repo push path. Existing empty repositories created via the API or web UI remain exploitable through first push until the code is patched.


Reported by zx (GitHub: @manus-pi).

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
Low
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N

CVE ID

CVE-2026-89182

Weaknesses

Incorrect Authorization

The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. Learn more on MITRE.

Credits